diff options
Diffstat (limited to 'activerecord/lib')
30 files changed, 308 insertions, 214 deletions
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 06bdabfced..513d1012ba 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -965,8 +965,8 @@ module ActiveRecord # For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they # cause the records in the join table to be removed. # - # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the - # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either + # For +has_many+, <tt>destroy</tt> and <tt>destory_all</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. # The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 3f0e4ca999..868095f068 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -146,7 +146,7 @@ module ActiveRecord def interpolate(sql, record = nil) if sql.respond_to?(:to_proc) - owner.send(:instance_exec, record, &sql) + owner.instance_exec(record, &sql) else sql end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 711f7b3ce1..5604687b57 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -70,7 +70,7 @@ module ActiveRecord end def instantiate(row) - @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row)) + @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row)) end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index b0b1c13b0d..c4b50ab306 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -47,12 +47,12 @@ module ActiveRecord through_scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_values.empty? - through_scope.includes_values = reflection_scope.values[:includes] || options[:source] + through_scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) through_scope.where_values = reflection_scope.values[:where] end - through_scope.order! reflection_scope.values[:order] through_scope.references! reflection_scope.values[:references] + through_scope.order! reflection_scope.values[:order] if through_scope.eager_loading? end through_scope diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 310f1b6e75..3e454b713a 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,27 +8,32 @@ module ActiveRecord # Returns this record's primary key value wrapped in an Array if one is # available. def to_key + sync_with_transaction_state key = self.id [key] if key end # Returns the primary key value. def id + sync_with_transaction_state read_attribute(self.class.primary_key) end # Sets the primary key value. def id=(value) + sync_with_transaction_state write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. def id? + sync_with_transaction_state query_attribute(self.class.primary_key) end # Returns the primary key value before type cast. def id_before_type_cast + sync_with_transaction_state read_attribute_before_type_cast(self.class.primary_key) end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 3c03cce838..506f5d75f9 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -74,7 +74,7 @@ module ActiveRecord # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) # If it's cached, just return it - # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829. + # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. name = attr_name.to_s @attributes_cache[name] || @attributes_cache.fetch(name) { column = @columns_hash.fetch(name) { diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 704998301c..55542262b0 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -347,7 +347,7 @@ module ActiveRecord end # reconstruct the scope now that we know the owner's id - association.send(:reset_scope) if association.respond_to?(:reset_scope) + association.reset_scope if association.respond_to?(:reset_scope) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 847d9da6e6..1754e424b8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -577,10 +577,10 @@ module ActiveRecord # When a connection is established or removed, we invalidate the cache. # # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil. - # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is - # significantly slower than #[]. So in the nil case, no caching will take place, - # but that's ok since the nil case is not the common one that we wish to optimise - # for. + # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that + # #fetch is significantly slower than #[]. So in the nil case, no caching will + # take place, but that's ok since the nil case is not the common one that we wish + # to optimise for. def retrieve_connection_pool(klass) class_to_pool[klass.name] ||= begin until pool = pool_for(klass) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 30ccb8f0a4..2859fb31e8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -17,6 +17,15 @@ module ActiveRecord 64 end + # Returns the maximum allowed length for an index name. This + # limit is enforced by rails and Is less than or equal to + # <tt>index_name_length</tt>. The gap between + # <tt>index_name_length</tt> is to allow internal rails + # opreations to use prefixes in temporary opreations. + def allowed_index_name_length + index_name_length + end + # Returns the maximum length of an index name. def index_name_length 64 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index cdc8433185..9bae880024 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -192,6 +192,14 @@ module ActiveRecord # Set to true to drop the table before creating it. # Defaults to false. # + # Note that +create_join_table+ does not create any indices by default; you can use + # its block form to do so yourself: + # + # create_join_table :products, :categories do |t| + # t.index :products + # t.index :categories + # end + # # ====== Add a backend specific option to the generated SQL (MySQL) # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') # generates: @@ -647,10 +655,11 @@ module ActiveRecord index_name = index_name(table_name, column: column_names) if Hash === options # legacy support, since this param was a string - options.assert_valid_keys(:unique, :order, :name, :where, :length) + options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal) index_type = options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if supports_partial_index? index_options = options[:where] ? " WHERE #{options[:where]}" : "" @@ -665,10 +674,11 @@ module ActiveRecord end index_type = options + max_index_length = allowed_index_name_length end - if index_name.length > index_name_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters" + if index_name.length > max_index_length + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end if index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" @@ -694,6 +704,28 @@ module ActiveRecord column_names.map {|column_name| quote_column_name(column_name) } end + def rename_table_indexes(table_name, new_name) + indexes(new_name).each do |index| + generated_index_name = index_name(table_name, column: index.columns) + if generated_index_name == index.name + rename_index new_name, generated_index_name, index_name(new_name, column: index.columns) + end + end + end + + def rename_column_indexes(table_name, column_name, new_column_name) + column_name, new_column_name = column_name.to_s, new_column_name.to_s + indexes(table_name).each do |index| + next unless index.columns.include?(new_column_name) + old_columns = index.columns.dup + old_columns[old_columns.index(new_column_name)] = column_name + generated_index_name = index_name(table_name, column: old_columns) + if generated_index_name == index.name + rename_index table_name, generated_index_name, index_name(table_name, column: index.columns) + end + end + end + private def table_definition TableDefinition.new(self) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 3ecef96b10..73c80a3220 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -5,7 +5,7 @@ module ActiveRecord def initialize(connection) @connection = connection - @state = TransactionState.new + @state = TransactionState.new end def state @@ -14,11 +14,13 @@ module ActiveRecord end class TransactionState + attr_accessor :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) def initialize(state = nil) @state = state + @parent = nil end def committed? @@ -116,7 +118,11 @@ module ActiveRecord end def add_record(record) - records << record + if record.has_transactional_callbacks? + records << record + else + record.set_transaction_state(@state) + end end def rollback_records @@ -188,8 +194,9 @@ module ActiveRecord end def perform_commit + @state.set_state(:committed) + @state.parent = parent.state connection.release_savepoint - records.each { |r| parent.add_record(r) } 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 eecf4faa5d..ff9de712bc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -61,12 +61,30 @@ module ActiveRecord include MonitorMixin include ColumnDumper + SIMPLE_INT = /\A\d+\z/ + define_callbacks :checkout, :checkin attr_accessor :visitor, :pool attr_reader :schema_cache, :last_use, :in_use, :logger alias :in_use? :in_use + def self.type_cast_config_to_integer(config) + if config =~ SIMPLE_INT + config.to_i + else + config + end + end + + def self.type_cast_config_to_boolean(config) + if config == "false" + false + else + config + end + end + def initialize(connection, logger = nil, pool = nil) #:nodoc: super() @@ -171,14 +189,14 @@ module ActiveRecord false end - # Does this adapter support database extensions? As of this writing - # only postgresql does. + # Does this adapter support database extensions? As of this writing only + # postgresql does. def supports_extensions? false end - # A list of extensions, to be filled in by databases that - # support them (at the moment, postgresql). + # A list of extensions, to be filled in by adapters that support them. At + # the moment only postgresql does. def extensions [] end @@ -326,10 +344,6 @@ module ActiveRecord # override in derived class ActiveRecord::StatementInvalid.new(message) end - - def valid_types?(type) - true - end end end 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 de5232f960..5480204511 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -31,7 +31,7 @@ module ActiveRecord return false if blob_or_text_column? #mysql forbids defaults on blob and text columns super end - + def blob_or_text_column? sql_type =~ /blob/i || type == :text end @@ -140,7 +140,7 @@ module ActiveRecord @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::MySQL.new self else @visitor = BindSubstitution.new self @@ -468,6 +468,7 @@ module ActiveRecord # rename_table('octopuses', 'octopi') def rename_table(table_name, new_name) execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + rename_table_indexes(table_name, new_name) end def add_column(table_name, column_name, type, options = {}) @@ -495,6 +496,7 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) #:nodoc: execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}") + rename_column_indexes(table_name, column_name, new_column_name) end # Maps logical Rails types to MySQL-specific data types. @@ -579,7 +581,7 @@ module ActiveRecord end def strict_mode? - @config.fetch(:strict, true) + self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end protected @@ -720,7 +722,7 @@ module ActiveRecord # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = wait_timeout + variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables @@ -747,10 +749,6 @@ module ActiveRecord # ...and send them all in one query execute("SET #{encoding} #{variable_assignments}", :skip_logging) end - - def valid_type?(type) - !native_database_types[type].nil? - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 577a362568..2c683fc3ac 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -38,7 +38,7 @@ module ActiveRecord private def resolve_string_connection(spec) # :nodoc: hash = configurations.fetch(spec) do |k| - connection_url_to_hash(k) + self.class.connection_url_to_hash(k) end raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash @@ -51,10 +51,13 @@ module ActiveRecord raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) + path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter" begin - require "active_record/connection_adapters/#{spec[:adapter]}_adapter" + require path_to_adapter + rescue Gem::LoadError => e + raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile." rescue LoadError => e - raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" @@ -62,11 +65,7 @@ module ActiveRecord ConnectionSpecification.new(spec, adapter_method) end - # For DATABASE_URL, accept a limited concept of ints and floats - SIMPLE_INT = /\A\d+\z/ - SIMPLE_FLOAT = /\A\d+\.\d+\z/ - - def connection_url_to_hash(url) # :nodoc: + def self.connection_url_to_hash(url) # :nodoc: config = URI.parse url adapter = config.scheme adapter = "postgresql" if adapter == "postgres" @@ -86,28 +85,11 @@ module ActiveRecord if config.query options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys - options.each { |key, value| options[key] = type_cast_value(value) } - spec.merge!(options) end spec end - - def type_cast_value(value) - case value - when SIMPLE_INT - value.to_i - when SIMPLE_FLOAT - value.to_f - when 'true' - true - when 'false' - false - else - value - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 631f646f58..7544c2a783 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -126,7 +126,7 @@ module ActiveRecord def initialize(connection, logger, connection_options, config) super @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @client_encoding = nil connect end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 73ca2c8e61..3bc61c5e0c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -321,14 +321,16 @@ module ActiveRecord # # Example: # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) + def rename_table(table_name, new_name) clear_cache! - execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq == "#{name}_#{pk}_seq" + if seq == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end + + rename_table_indexes(table_name, new_name) end # Adds a new column to the named table. @@ -370,6 +372,7 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" + rename_column_indexes(table_name, column_name, new_column_name) end def remove_index!(table_name, index_name) #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 271a6848ee..2bb2557efd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -478,7 +478,7 @@ module ActiveRecord def initialize(connection, logger, connection_parameters, config) super(connection, logger) - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::PostgreSQL.new self else @visitor = BindSubstitution.new self @@ -492,7 +492,7 @@ module ActiveRecord connect @statements = StatementPool.new @connection, - config.fetch(:statement_limit) { 1000 } + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) if postgresql_version < 80200 raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" @@ -500,7 +500,7 @@ module ActiveRecord initialize_type_map @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] - @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true + @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true end # Clears the prepared statements cache. @@ -887,10 +887,6 @@ module ActiveRecord def table_definition TableDefinition.new(self) end - - def valid_type?(type) - !native_database_types[type].nil? - 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 b644e7bd60..981c4c96a0 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -26,7 +26,7 @@ module ActiveRecord :results_as_hash => true ) - db.busy_timeout(config[:timeout]) if config[:timeout] + db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] ConnectionAdapters::SQLite3Adapter.new(db, logger, config) end @@ -107,10 +107,10 @@ module ActiveRecord @active = nil @statements = StatementPool.new(@connection, - config.fetch(:statement_limit) { 1000 }) + self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config - if config.fetch(:prepared_statements) { true } + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @visitor = Arel::Visitors::SQLite.new self else @visitor = BindSubstitution.new self @@ -187,6 +187,13 @@ module ActiveRecord true end + # Returns 62. SQLite supports index names up to 64 + # characters. The rest is used by rails internally to perform + # temporary rename operations + def allowed_index_name_length + index_name_length - 2 + end + def native_database_types #:nodoc: { :primary_key => default_primary_key_type, @@ -428,8 +435,9 @@ module ActiveRecord # # Example: # rename_table('octopuses', 'octopi') - def rename_table(name, new_name) - exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}" + def rename_table(table_name, new_name) + exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" + rename_table_indexes(table_name, new_name) end # See: http://www.sqlite.org/lang_altertable.html @@ -488,6 +496,7 @@ module ActiveRecord raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}" end alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) + rename_column_indexes(table_name, column_name, new_column_name) end protected @@ -502,7 +511,7 @@ module ActiveRecord end def alter_table(table_name, options = {}) #:nodoc: - altered_table_name = "altered_#{table_name}" + altered_table_name = "a#{table_name}" caller = lambda {|definition| yield definition if block_given?} transaction do @@ -546,10 +555,10 @@ module ActiveRecord def copy_table_indexes(from, to, rename = {}) #:nodoc: indexes(from).each do |index| name = index.name - if to == "altered_#{from}" - name = "temp_#{name}" - elsif from == "altered_#{to}" - name = name[5..-1] + if to == "a#{from}" + name = "t#{name}" + elsif from == "a#{to}" + name = name[1..-1] end to_column_names = columns(to).map { |c| c.name } @@ -559,7 +568,7 @@ module ActiveRecord unless columns.empty? # index name can't be the same - opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_") } + opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true } opts[:unique] = true if index.unique add_index(to, columns, opts) end @@ -602,11 +611,6 @@ module ActiveRecord super end end - - def valid_type?(type) - true - end - end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 63a1197a56..899fe7d7c7 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -249,7 +249,6 @@ module ActiveRecord @new_record = true ensure_proper_type - populate_with_current_scope_attributes super end @@ -347,8 +346,54 @@ module ActiveRecord Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access end + def set_transaction_state(state) # :nodoc: + @transaction_state = state + end + + def has_transactional_callbacks? # :nodoc: + !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty? + end + private + # Updates the attributes on this particular ActiveRecord object so that + # if it is associated with a transaction, then the state of the AR object + # will be updated to reflect the current state of the transaction + # + # The @transaction_state variable stores the states of the associated + # transaction. This relies on the fact that a transaction can only be in + # one rollback or commit (otherwise a list of states would be required) + # Each AR object inside of a transaction carries that transaction's + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the ActiveRecord object + # as appropriate. + # + # Since ActiveRecord objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the ActiveRecord object reflects the state of the object. + def sync_with_transaction_state + update_attributes_from_transaction_state(@transaction_state, 0) + end + + def update_attributes_from_transaction_state(transaction_state, depth) + if transaction_state && !has_transactional_callbacks? + unless @reflects_state[depth] + if transaction_state.committed? + committed! + elsif transaction_state.rolledback? + rolledback! + end + @reflects_state[depth] = true + end + + if transaction_state.parent && !@reflects_state[depth+1] + update_attributes_from_transaction_state(transaction_state.parent, depth+1) + end + end + end + # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements # of the array, and then rescues from the possible NoMethodError. If those elements are # ActiveRecord::Base's, then this triggers the various method_missing's that we have, @@ -376,7 +421,8 @@ module ActiveRecord @new_record = true @txn = nil @_start_transaction_state = {} - @transaction = nil + @transaction_state = nil + @reflects_state = [false] end end end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 70683eb731..b2a9a54af1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -2,43 +2,7 @@ require 'active_support/lazy_load_hooks' module ActiveRecord module Explain - def self.extended(base) - base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false - end - - # If the database adapter supports explain and auto explain is enabled, - # this method triggers EXPLAIN logging for the queries triggered by the - # block if it takes more than the threshold as a whole. That is, the - # threshold is not checked against each individual query, but against the - # duration of the entire block. This approach is convenient for relations. - - # - # The available_queries_for_explain thread variable collects the queries - # to be explained. If the value is nil, it means queries are not being - # currently collected. A false value indicates collecting is turned - # off. Otherwise it is an array of queries. - def logging_query_plan # :nodoc: - return yield unless logger - - threshold = auto_explain_threshold_in_seconds - current = Thread.current - if connection.supports_explain? && threshold && current[:available_queries_for_explain].nil? - begin - queries = current[:available_queries_for_explain] = [] - start = Time.now - result = yield - logger.warn(exec_explain(queries)) if Time.now - start > threshold - result - ensure - current[:available_queries_for_explain] = nil - end - else - yield - end - end - - # Relation#explain needs to be able to collect the queries regardless of - # whether auto explain is enabled. This method serves that purpose. + # Relation#explain needs to be able to collect the queries. def collecting_queries_for_explain # :nodoc: current = Thread.current original, current[:available_queries_for_explain] = current[:available_queries_for_explain], [] @@ -68,20 +32,5 @@ module ActiveRecord end str end - - # Silences automatic EXPLAIN logging for the duration of the block. - # - # This has high priority, no EXPLAINs will be run even if downwards - # the threshold is set to 0. - # - # As the name of the method suggests this only applies to automatic - # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run. - def silence_auto_explain - current = Thread.current - original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false - yield - ensure - current[:available_queries_for_explain] = original - end end end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 7f877a6471..32d35f0ec1 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -5,8 +5,10 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp format in the cache key. - # This is +:number+, by default. + # Indicates the format used to generate the timestamp in the cache key. + # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # + # This is +:nsec+, by default. class_attribute :cache_timestamp_format, :instance_writer => false self.cache_timestamp_format = :nsec end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 803cae7115..347f023793 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -69,11 +69,13 @@ module ActiveRecord # Returns true if this object hasn't been saved yet -- that is, a record # for the object doesn't exist in the data store yet; otherwise, returns false. def new_record? + sync_with_transaction_state @new_record end # Returns true if this object has been destroyed, otherwise returns false. def destroyed? + sync_with_transaction_state @destroyed end @@ -365,7 +367,16 @@ module ActiveRecord # # # triggers @brake.car.touch and @brake.car.corporation.touch # @brake.touch + # + # Note that +touch+ must be used on a persisted object, or else an + # ActiveRecordError will be thrown. For example: + # + # ball = Ball.new + # ball.touch(:updated_at) # => raises ActiveRecordError + # def touch(name = nil) + raise ActiveRecordError, "can not touch on a new record object" unless persisted? + attributes = timestamp_attributes_for_update_in_model attributes << name if name @@ -418,13 +429,22 @@ module ActiveRecord # Returns the number of affected rows. def update_record(attribute_names = @attributes.keys) attributes_with_values = arel_attributes_with_values_for_update(attribute_names) - if attributes_with_values.empty? 0 else klass = self.class - stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) - klass.connection.update stmt + column_hash = klass.connection.schema_cache.columns_hash klass.table_name + db_columns_with_values = attributes_with_values.map { |attr,value| + real_column = column_hash[attr.name] + [real_column, value] + } + bind_attrs = attributes_with_values.dup + bind_attrs.keys.each_with_index do |column, i| + real_column = db_columns_with_values[i].first + bind_attrs[column] = klass.connection.substitute_at(real_column, i) + end + stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(bind_attrs) + klass.connection.update stmt, 'SQL', db_columns_with_values end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index f08b9c614d..e04a3d0976 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -33,18 +33,16 @@ module ActiveRecord # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # # => [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...] def find_by_sql(sql, binds = []) - logging_query_plan do - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) - column_types = {} + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + column_types = {} - if result_set.respond_to? :column_types - column_types = result_set.column_types - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end - - result_set.map { |record| instantiate(record, column_types) } + if result_set.respond_to? :column_types + column_types = result_set.column_types + else + ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" end + + result_set.map { |record| instantiate(record, column_types) } end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. @@ -57,10 +55,8 @@ module ActiveRecord # # Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id" def count_by_sql(sql) - logging_query_plan do - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i - end + sql = sanitize_conditions(sql) + connection.select_value(sql, "#{name} Count").to_i end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index aceb70bc45..64eac3aca7 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -112,7 +112,18 @@ module ActiveRecord `config/application.rb` file and any `mass_assignment_sanitizer` options from your `config/environments/*.rb` files. - See http://guides.rubyonrails.org/security.html#mass-assignment for more information + See http://guides.rubyonrails.org/security.html#mass-assignment for more information. + EOF + end + + unless app.config.active_record.delete(:auto_explain_threshold_in_seconds).nil? + ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, [] + The Active Record auto explain feature has been removed. + + To disable this message remove the `active_record.auto_explain_threshold_in_seconds` + option from the `config/environments/*.rb` config file. + + See http://guides.rubyonrails.org/4_0_release_notes.html for more information. EOF end @@ -124,7 +135,7 @@ module ActiveRecord To disable this message remove the `observers` option from your `config/application.rb` or from your initializers. - See http://guides.rubyonrails.org/4_0_release_notes.html for more information + See http://guides.rubyonrails.org/4_0_release_notes.html for more information. EOF end ensure @@ -141,20 +152,11 @@ module ActiveRecord # and then establishes the connection. initializer "active_record.initialize_database" do |app| ActiveSupport.on_load(:active_record) do - unless ENV['DATABASE_URL'] - self.configurations = app.config.database_configuration - end + self.configurations = app.config.database_configuration establish_connection end end - initializer "active_record.validate_explain_support" do |app| - if app.config.active_record[:auto_explain_threshold_in_seconds] && - !ActiveRecord::Base.connection.supports_explain? - warn "auto_explain_threshold_in_seconds is set but will be ignored because your adapter does not support this feature. Please unset the configuration to avoid this warning." - end - end - # Expose database runtime to controller for logging. initializer "active_record.log_runtime" do |app| require "active_record/railties/controller_runtime" diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0053530f73..bc50802c4a 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -444,17 +444,7 @@ module ActiveRecord # # Post.where(published: true).load # => #<ActiveRecord::Relation> def load - unless loaded? - # We monitor here the entire execution rather than individual SELECTs - # because from the point of view of the user fetching the records of a - # relation is a single unit of work. You want to know if this call takes - # too long, not if the individual queries take too long. - # - # It could be the case that none of the queries involved surpass the - # threshold, and at the same time the sum of them all does. The user - # should get a query plan logged in that case. - logging_query_plan { exec_queries } - end + exec_queries unless loaded? self end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 615309964c..00a506c3a7 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -12,7 +12,7 @@ module ActiveRecord delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, - :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass + :connection, :columns_hash, :to => :klass module ClassSpecificRelation extend ActiveSupport::Concern diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7ddaea1bb0..14520381c9 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -58,7 +58,7 @@ module ActiveRecord # order. The order will depend on the database implementation. # If an order is supplied it will be respected. # - # Person.take # returns an object fetched by SELECT * FROM people + # Person.take # returns an object fetched by SELECT * FROM people LIMIT 1 # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 # Person.where(["name LIKE '%?'", name]).take def take(limit = nil) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 63836bf375..4b8c40592e 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -11,11 +11,11 @@ module ActiveRecord @scope = scope end - # Returns a new relation expressing WHERE + NOT condition - # according to the conditions in the arguments. + # Returns a new relation expressing WHERE + NOT condition according to + # the conditions in the arguments. # - # #not accepts conditions in one of these formats: String, Array, Hash. - # See #where for more details on each format. + # +not+ accepts conditions as a string, array, or hash. See #where for + # more details on each format. # # User.where.not("name = 'Jon'") # # SELECT * FROM users WHERE NOT (name = 'Jon') @@ -31,6 +31,10 @@ module ActiveRecord # # User.where.not(name: %w(Ko1 Nobu)) # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu') + # + # User.where.not(name: "Jon", role: "admin") + # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' + # def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel @@ -108,7 +112,8 @@ module ActiveRecord # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) def includes(*args) - args.empty? ? self : spawn.includes!(*args) + check_if_method_has_arguments!("includes", args) + spawn.includes!(*args) end def includes!(*args) # :nodoc: @@ -125,7 +130,8 @@ module ActiveRecord # FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = # "users"."id" def eager_load(*args) - args.blank? ? self : spawn.eager_load!(*args) + check_if_method_has_arguments!("eager_load", args) + spawn.eager_load!(*args) end def eager_load!(*args) # :nodoc: @@ -138,7 +144,8 @@ module ActiveRecord # User.preload(:posts) # => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3) def preload(*args) - args.blank? ? self : spawn.preload!(*args) + check_if_method_has_arguments!("preload", args) + spawn.preload!(*args) end def preload!(*args) # :nodoc: @@ -155,7 +162,8 @@ module ActiveRecord # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN def references(*args) - args.blank? ? self : spawn.references!(*args) + check_if_method_has_arguments!("references", args) + spawn.references!(*args) end def references!(*args) # :nodoc: @@ -234,7 +242,8 @@ module ActiveRecord # User.group('name AS grouped_name, age') # => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>] def group(*args) - args.blank? ? self : spawn.group!(*args) + check_if_method_has_arguments!("group", args) + spawn.group!(*args) end def group!(*args) # :nodoc: @@ -264,7 +273,8 @@ module ActiveRecord # User.order(:name, email: :desc) # => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC def order(*args) - args.blank? ? self : spawn.order!(*args) + check_if_method_has_arguments!("order", args) + spawn.order!(*args) end def order!(*args) # :nodoc: @@ -275,6 +285,11 @@ module ActiveRecord references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! references!(references) if references.any? + # if a symbol is given we prepend the quoted table name + args = args.map { |arg| + arg.is_a?(Symbol) ? "#{quoted_table_name}.#{arg} ASC" : arg + } + self.order_values = args + self.order_values self end @@ -289,7 +304,8 @@ module ActiveRecord # # generates a query with 'ORDER BY name ASC, id ASC'. def reorder(*args) - args.blank? ? self : spawn.reorder!(*args) + check_if_method_has_arguments!("reorder", args) + spawn.reorder!(*args) end def reorder!(*args) # :nodoc: @@ -311,7 +327,8 @@ module ActiveRecord # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id") # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) - args.compact.blank? ? self : spawn.joins!(*args.flatten) + check_if_method_has_arguments!("joins", args) + spawn.joins!(*args.compact.flatten) end def joins!(*args) # :nodoc: @@ -457,8 +474,6 @@ module ActiveRecord end end - # #where! is identical to #where, except that instead of returning a new relation, it adds - # the condition to the existing relation. def where!(opts = :chain, *rest) # :nodoc: if opts == :chain WhereChain.new(self) @@ -476,6 +491,7 @@ module ActiveRecord # Order.having('SUM(price) > 30').group('user_id') def having(opts, *rest) opts.blank? ? self : spawn.having!(opts, *rest) + spawn.having!(opts, *rest) end def having!(opts, *rest) # :nodoc: @@ -623,7 +639,6 @@ module ActiveRecord spawn.from!(value, subquery_name) end - # Like #from, but modifies relation in place. def from!(value, subquery_name = nil) # :nodoc: self.from_value = [value, subquery_name] self @@ -912,5 +927,26 @@ module ActiveRecord end end + # Checks to make sure that the arguments are not blank. Note that if some + # blank-like object were initially passed into the query method, then this + # method will not raise an error. + # + # Example: + # + # Post.references() # => raises an error + # Post.references([]) # => does not raise an error + # + # This particular method should be called with a method_name and the args + # passed into that method as an input. For example: + # + # def references(*args) + # check_if_method_has_arguments!("references", args) + # ... + # end + def check_if_method_has_arguments!(method_name, args) + if args.blank? + raise ArgumentError, "The method .#{method_name}() must contain arguments." + end + end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 4b7a388dc7..33718ef0e9 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -218,9 +218,8 @@ module ActiveRecord # after_commit :do_bar, on: :update # after_commit :do_baz, on: :destroy # - # Also, to have the callback fired on create and update, but not on destroy: - # - # after_commit :do_zoo, if: :persisted? + # after_commit :do_foo_bar, :on [:create, :update] + # after_commit :do_bar_baz, :on [:update, :destroy] # # Note that transactional fixtures do not play well with this feature. Please # use the +test_after_commit+ gem to have these hooks fired in tests. @@ -244,12 +243,14 @@ module ActiveRecord if options.is_a?(Hash) && options[:on] assert_valid_transaction_action(options[:on]) options[:if] = Array(options[:if]) - options[:if] << "transaction_include_action?(:#{options[:on]})" + fire_on = Array(options[:on]).map(&:to_sym) + options[:if] << "transaction_include_any_action?(#{fire_on})" end end - def assert_valid_transaction_action(action) - unless ACTIONS.include?(action.to_sym) + def assert_valid_transaction_action(actions) + actions = Array(actions) + if (actions - ACTIONS).any? raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" end end @@ -378,14 +379,16 @@ module ActiveRecord end # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_action?(action) #:nodoc: - case action - when :create - transaction_record_state(:new_record) - when :destroy - destroyed? - when :update - !(transaction_record_state(:new_record) || destroyed?) + def transaction_include_any_action?(actions) #:nodoc: + actions.any? do |action| + case action + when :create + transaction_record_state(:new_record) + when :destroy + destroyed? + when :update + !(transaction_record_state(:new_record) || destroyed?) + end end end end diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb index 0c35adc11d..c0471bb506 100644 --- a/activerecord/lib/active_record/version.rb +++ b/activerecord/lib/active_record/version.rb @@ -3,7 +3,7 @@ module ActiveRecord MAJOR = 4 MINOR = 0 TINY = 0 - PRE = "beta" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end |