diff options
author | Jon Leighton <j@jonathanleighton.com> | 2010-10-28 18:06:01 +0100 |
---|---|---|
committer | Jon Leighton <j@jonathanleighton.com> | 2010-10-28 18:06:01 +0100 |
commit | fc276e5635821e65c04b8961170cc6bd3c11b923 (patch) | |
tree | 9bd4ed018858611248e9b6aa7392dced92bc814a /activerecord/lib | |
parent | 8e53e058acea471eab7a1609dc150aa9fdbfa833 (diff) | |
parent | 6a3d6b7f1352efd3e7b931533740252b04850e27 (diff) | |
download | rails-fc276e5635821e65c04b8961170cc6bd3c11b923.tar.gz rails-fc276e5635821e65c04b8961170cc6bd3c11b923.tar.bz2 rails-fc276e5635821e65c04b8961170cc6bd3c11b923.zip |
Merge branch 'master' into nested_has_many_through
Conflicts:
activerecord/CHANGELOG
activerecord/lib/active_record/association_preload.rb
activerecord/lib/active_record/associations.rb
activerecord/test/schema/schema.rb
Diffstat (limited to 'activerecord/lib')
17 files changed, 386 insertions, 126 deletions
diff --git a/activerecord/lib/active_record/association_preload.rb b/activerecord/lib/active_record/association_preload.rb index 664b0a7d59..bafd59fc02 100644 --- a/activerecord/lib/active_record/association_preload.rb +++ b/activerecord/lib/active_record/association_preload.rb @@ -254,8 +254,6 @@ module ActiveRecord alias_method :preload_has_many_association, :preload_has_one_or_has_many_association def preload_through_records(records, reflection, through_association) - through_reflection = reflections[through_association] - # If the same through record is loaded twice, we want to return exactly the same # object in the result, rather than two separate instances representing the same # record. This is so that we can preload the source association for each record, diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 25be9a52ff..ee2e2765bc 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -2043,7 +2043,7 @@ module ActiveRecord association_proxy = record.send("set_#{join_part.reflection.name}_target", association) association_proxy.__send__(:set_inverse_instance, association, record) end - + # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited # by JoinBase and JoinAssociation. A JoinBase represents the Active Record which # everything else is being joined onto. A JoinAssociation represents an association which @@ -2055,38 +2055,38 @@ module ActiveRecord # this is the actual base model, for a JoinAssociation this is the target model of the # association. attr_reader :active_record - + delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :arel_engine, :to => :active_record - + def initialize(active_record) @active_record = active_record @cached_record = {} end - + def ==(other) raise NotImplementedError end - + # An Arel::Table for the active_record def table raise NotImplementedError end - + # The prefix to be used when aliasing columns in the active_record's table def aliased_prefix raise NotImplementedError end - + # The alias for the active_record's table def aliased_table_name raise NotImplementedError end - + # The alias for the primary key of the active_record's table def aliased_primary_key "#{aliased_prefix}_r0" end - + # An array of [column_name, alias] pairs for the table def column_names_with_alias unless defined?(@column_names_with_alias) @@ -2116,7 +2116,7 @@ module ActiveRecord class JoinBase < JoinPart # :nodoc: # Extra joins provided when the JoinDependency was created attr_reader :table_joins - + def initialize(active_record, joins = nil) super(active_record) @table_joins = joins @@ -2144,16 +2144,16 @@ module ActiveRecord class JoinAssociation < JoinPart # :nodoc: # The reflection of the association represented attr_reader :reflection - + # The JoinDependency object which this JoinAssociation exists within. This is mainly # relevant for generating aliases which do not conflict with other joins which are # part of the query. attr_reader :join_dependency - + # A JoinBase instance representing the active record we are joining onto. # (So in Author.has_many :posts, the Author would be that base record.) attr_reader :parent - + # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin attr_accessor :join_type @@ -2165,7 +2165,7 @@ module ActiveRecord def initialize(reflection, join_dependency, parent = nil) reflection.check_validity! - + if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) end @@ -2192,7 +2192,7 @@ module ActiveRecord self.parent == join_part end end - + def join_to(relation) # The chain starts with the target table, but we want to end with it here (makes # more sense in this context) @@ -2282,7 +2282,7 @@ module ActiveRecord self.join_type = Arel::OuterJoin joining_relation.joins(self) end - + def table if @tables.last.is_a?(Array) @tables.last.first diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index f3f89fe7c3..0b89a49896 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -167,7 +167,16 @@ module ActiveRecord else if reflection.macro == :has_one define_method(save_method) { save_has_one_association(reflection) } - after_save save_method + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method else define_method(save_method) { save_belongs_to_association(reflection) } before_save save_method diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 78b3507dd9..06a388cd21 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -448,8 +448,8 @@ module ActiveRecord #:nodoc: # # You can use the same string replacement techniques as you can with ActiveRecord#find # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # > [#<Post:0x36bff9c @attributes={"first_name"=>"The Cheap Man Buys Twice"}>, ...] - def find_by_sql(sql) - connection.select_all(sanitize_sql(sql), "#{name} Load").collect! { |record| instantiate(record) } + def find_by_sql(sql, binds = []) + connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) } end # Creates an object (or multiple objects) and saves it to the database, if validations pass. @@ -718,6 +718,7 @@ module ActiveRecord #:nodoc: # end # end def reset_column_information + connection.clear_cache! undefine_attribute_methods @column_names = @columns = @columns_hash = @content_columns = @dynamic_methods_hash = @inheritance_column = nil @arel_engine = @relation = @arel_table = nil @@ -972,14 +973,11 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.scope? self.class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) - options = args.extract_options! # options = args.extract_options! - attributes = construct_attributes_from_arguments( # attributes = construct_attributes_from_arguments( - [:#{attribute_names.join(',:')}], args # [:user_name, :password], args - ) # ) - # - scoped(:conditions => attributes) # scoped(:conditions => attributes) - end # end + def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args) + attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)] + # + scoped(:conditions => attributes) # scoped(:conditions => attributes) + end # end METHOD send(method_id, *arguments) end @@ -988,31 +986,22 @@ module ActiveRecord #:nodoc: end end - def construct_attributes_from_arguments(attribute_names, arguments) - attributes = {} - attribute_names.each_with_index { |name, idx| attributes[name] = arguments[idx] } - attributes - end - # Similar in purpose to +expand_hash_conditions_for_aggregates+. def expand_attribute_names_for_aggregates(attribute_names) - expanded_attribute_names = [] - attribute_names.each do |attribute_name| + attribute_names.map { |attribute_name| unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil? - aggregate_mapping(aggregation).each do |field_attr, aggregate_attr| - expanded_attribute_names << field_attr + aggregate_mapping(aggregation).map do |field_attr, _| + field_attr.to_sym end else - expanded_attribute_names << attribute_name + attribute_name.to_sym end - end - expanded_attribute_names + }.flatten end def all_attributes_exists?(attribute_names) - expand_attribute_names_for_aggregates(attribute_names).all? { |name| - column_methods_hash.include?(name.to_sym) - } + (expand_attribute_names_for_aggregates(attribute_names) - + column_methods_hash.keys).empty? end protected @@ -1139,11 +1128,17 @@ MSG # Article.new.published # => true # Article.create.published # => true def default_scope(options = {}) + reset_scoped_methods self.default_scoping << construct_finder_arel(options, default_scoping.pop) end def current_scoped_methods #:nodoc: - scoped_methods.last + method = scoped_methods.last + if method.respond_to?(:call) + relation.scoping { method.call } + else + method + end end def reset_scoped_methods #:nodoc: 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 c0cc7ba20d..a4d7d12298 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 @@ -121,7 +133,7 @@ module ActiveRecord # Quote date/time values for use in SQL input. Includes microseconds # if the value is a Time responding to usec. def quoted_date(value) #:nodoc: - if value.acts_like?(:time) && value.respond_to?(:usec) + if value.respond_to?(:usec) "#{super}.#{sprintf("%06d", value.usec)}" else super @@ -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| + exec("PRAGMA index_info('#{row['name']}')").map { |col| col['name'] }) end @@ -208,11 +241,11 @@ module ActiveRecord 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,8 +313,8 @@ module ActiveRecord end protected - def select(sql, name = nil) #:nodoc: - execute(sql, name).map do |row| + def select(sql, name = nil, binds = []) #:nodoc: + exec(sql, name, binds).map do |row| record = {} row.each do |key, value| record[key.sub(/^"?\w+"?\./, '')] = value if key.is_a?(String) @@ -367,11 +400,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 diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index 6ab84df25b..0b92ba5caa 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -97,14 +97,14 @@ module ActiveRecord # # Article.published.new.published # => true # Article.published.create.published # => true - def scope(name, scope_options = {}, &block) + def scope(name, scope_options = {}) name = name.to_sym valid_scope_name?(name) - extension = Module.new(&block) if block_given? + extension = Module.new(&Proc.new) if block_given? scopes[name] = lambda do |*args| - options = scope_options.is_a?(Proc) ? scope_options.call(*args) : scope_options + options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options relation = if options.is_a?(Hash) scoped.apply_finder_options(options) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 824674ee1d..6eb2057f66 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -36,7 +36,7 @@ module ActiveRecord # Returns an array of AggregateReflection objects for all the aggregations in the class. def reflect_on_all_aggregations - reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) } + reflections.values.grep(AggregateReflection) end # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). @@ -58,7 +58,7 @@ module ActiveRecord # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # def reflect_on_all_associations(macro = nil) - association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) } + association_reflections = reflections.values.grep(AssociationReflection) macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f129b54f9a..58f7b74198 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -5,7 +5,7 @@ module ActiveRecord class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] - MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] + MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -61,7 +61,7 @@ module ActiveRecord def to_a return @records if loaded? - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) preload = @preload_values preload += @includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index b763e22ec6..fe1ef2e2e3 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -288,7 +288,12 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id - record = where(primary_key.eq(id)).first + column = primary_key.column + + substitute = connection.substitute_for(column, @bind_values) + relation = where(primary_key.eq(substitute)) + relation.bind_values = [[column, id]] + record = relation.first unless record conditions = arel.where_sql diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 59ce76ea42..9c399d3333 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -6,7 +6,8 @@ module ActiveRecord extend ActiveSupport::Concern attr_accessor :includes_values, :eager_load_values, :preload_values, - :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, + :select_values, :group_values, :order_values, :joins_values, + :where_values, :having_values, :bind_values, :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value def includes(*args) @@ -62,6 +63,12 @@ module ActiveRecord relation end + def bind(value) + relation = clone + relation.bind_values += [value] + relation + end + def where(opts, *rest) relation = clone relation.where_values += build_where(opts, rest) unless opts.blank? @@ -117,8 +124,10 @@ module ActiveRecord relation end - def extending(*modules, &block) - modules << Module.new(&block) if block_given? + def extending(*modules) + modules << Module.new(&Proc.new) if block_given? + + return self if modules.empty? relation = clone relation.send(:apply_modules, modules.flatten) @@ -233,7 +242,7 @@ module ActiveRecord @implicit_readonly = false arel.project(*selects) else - arel.project(Arel::SqlLiteral.new(@klass.quoted_table_name + '.*')) + arel.project(Arel.sql(@klass.quoted_table_name + '.*')) end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index dcddc3dac4..648a02f1cc 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -26,19 +26,23 @@ module ActiveRecord merged_relation = merged_relation.joins(r.joins_values) - merged_wheres = @where_values + merged_wheres = @where_values + r.where_values - r.where_values.each do |w| + # Remove duplicates, last one wins. + seen = {} + merged_wheres = merged_wheres.reverse.reject { |w| + nuke = false if w.respond_to?(:operator) && w.operator == :== - merged_wheres = merged_wheres.reject {|p| p.respond_to?(:operator) && p.operator == :== && p.operand1.name == w.operand1.name } + name = w.left.name + nuke = seen[name] + seen[name] = true end - - merged_wheres += [w] - end + nuke + }.reverse merged_relation.where_values = merged_wheres - Relation::SINGLE_VALUE_METHODS.reject {|m| m == :lock}.each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock]).each do |method| value = r.send(:"#{method}_value") merged_relation.send(:"#{method}_value=", value) unless value.nil? end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb new file mode 100644 index 0000000000..8deff1478f --- /dev/null +++ b/activerecord/lib/active_record/result.rb @@ -0,0 +1,30 @@ +module ActiveRecord + ### + # This class encapsulates a Result returned from calling +exec+ on any + # database connection adapter. For example: + # + # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x # => #<ActiveRecord::Result:0xdeadbeef> + class Result + include Enumerable + + attr_reader :columns, :rows + + def initialize(columns, rows) + @columns = columns + @rows = rows + @hash_rows = nil + end + + def each + hash_rows.each { |row| yield row } + end + + private + def hash_rows + @hash_rows ||= @rows.map { |row| + ActiveSupport::OrderedHash[@columns.zip(row)] + } + end + end +end |