diff options
Diffstat (limited to 'activerecord')
63 files changed, 1839 insertions, 1631 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e8d4b9c04e..700e11ff94 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *Rails 3.2.0 (unreleased)* +* Support bulk change_table in mysql2 adapter, as well as the mysql one. [Jon Leighton] + * If multiple parameters are sent representing a date, and some are blank, the resulting object is nil. In previous releases those values defaulted to 1. This only affects existing but blank parameters, missing ones still raise an error. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 8c5c544773..b5db57569c 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -197,7 +197,7 @@ Admit the Database: == Download and installation -The latest version of Active Record can be installed with Rubygems: +The latest version of Active Record can be installed with RubyGems: % [sudo] gem install activerecord diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index c91a6ccd35..2de81c31a3 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 2.2.0') + s.add_dependency('arel', '~> 2.2.1') s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d63b9e3f24..132dc12680 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -21,13 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ - -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) - -activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__) -$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path) - require 'active_support' require 'active_support/i18n' require 'active_model' @@ -42,7 +35,7 @@ module ActiveRecord autoload :ActiveRecordError, 'active_record/errors' autoload :ConnectionNotEstablished, 'active_record/errors' autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter' - + autoload :Aggregations autoload :Associations autoload :AttributeMethods @@ -72,6 +65,7 @@ module ActiveRecord autoload :Persistence autoload :QueryCache autoload :Reflection + autoload :Result autoload :Schema autoload :SchemaDumper autoload :Serialization diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 2605a54cb6..8d755b6848 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1087,7 +1087,8 @@ module ActiveRecord # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex - # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ + # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is + # required. Note: When this option is used, +find_in_collection+ # is _not_ added. # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is @@ -1162,11 +1163,14 @@ module ActiveRecord # has_many :tags, :as => :taggable # has_many :reports, :readonly => true # has_many :subscribers, :through => :subscriptions, :source => :user - # has_many :subscribers, :class_name => "Person", :finder_sql => - # 'SELECT DISTINCT people.* ' + - # 'FROM people p, post_subscriptions ps ' + - # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + - # 'ORDER BY p.first_name' + # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { + # %Q{ + # SELECT DISTINCT people.* + # FROM people p, post_subscriptions ps + # WHERE ps.post_id = #{id} AND ps.person_id = p.id + # ORDER BY p.first_name + # } + # } def has_many(name, options = {}, &extension) Builder::HasMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 92ed844a2e..0248c7483c 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -53,12 +53,18 @@ module ActiveRecord # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase quoted_name = connection.quote_table_name(name).downcase - table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum + counts = table_joins.map do |join| + if join.is_a?(Arel::Nodes::StringJoin) + # Table names + table aliases + join.left.downcase.scan( + /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ + ).size + else + join.left.table_name == name ? 1 : 0 + end + end + + counts.sum end def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index bb519c5703..d1e3ff8e38 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -151,20 +151,20 @@ module ActiveRecord reset end + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + owner.send(:instance_exec, record, &sql) + else + sql + end + end + private def find_target? !loaded? && (!owner.new_record? || foreign_key_present?) && klass end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.send(:instance_exec, record, &sql) - else - sql - end - end - def creation_attributes attributes = {} diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index adb6af7165..58c9648ce8 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -20,6 +20,10 @@ module ActiveRecord private + def find_target? + !loaded? && foreign_key_present? && klass + end + def update_counters(record) counter_cache_name = reflection.counter_cache_column diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 198ad06360..2ee5dbbd70 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: + def klass + type = owner[reflection.foreign_type] + type.presence && type.constantize + end + private def replace_keys(record) @@ -17,11 +22,6 @@ module ActiveRecord reflection.polymorphic_inverse_of(record.class) end - def klass - type = owner[reflection.foreign_type] - type.presence && type.constantize - end - def raise_on_type_mismatch(record) # A polymorphic association cannot have a type mismatch, by definition end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 504f25271c..6c878f0f00 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -188,13 +188,12 @@ module ActiveRecord association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else - return if row[join_part.aliased_primary_key].nil? - association = join_part.instantiate(row) + association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? case macro when :has_many, :has_and_belongs_to_many other = record.association(join_part.reflection.name) other.loaded! - other.target.push(association) + other.target.push(association) if association other.set_inverse_instance(association) when :belongs_to set_target_and_inverse(join_part, association, record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 81172179e0..b010b5e9ef 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -16,7 +16,7 @@ module ActiveRecord chain[1..-1].each do |reflection| scope = scope.merge( reflection.klass.scoped.with_default_scope. - except(:select, :create_with, :includes) + except(:select, :create_with, :includes, :preload) ) end scope diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 102d8f4175..374791deb1 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -177,6 +177,10 @@ module ActiveRecord #:nodoc: # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do # <tt>Person.find_all_by_last_name(last_name)</tt>. # + # It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an + # <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records, + # like <tt>Person.find_by_last_name!</tt>. + # # It's also possible to use multiple attributes in the same find by separating them with "_and_". # # Person.where(:user_name => user_name, :password => password).first @@ -620,6 +624,8 @@ module ActiveRecord #:nodoc: # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: + return if abstract_class? + self.table_name = compute_table_name end @@ -1256,27 +1262,43 @@ MSG self.default_scopes = default_scopes + [scope] end - # The @ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... def build_default_scope #:nodoc: - return if defined?(@ignore_default_scope) && @ignore_default_scope - @ignore_default_scope = true - if method(:default_scope).owner != Base.singleton_class - default_scope + evaluate_default_scope { default_scope } elsif default_scopes.any? - default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) - else - default_scope.merge(scope) + evaluate_default_scope do + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) + else + default_scope.merge(scope) + end end end end - ensure - @ignore_default_scope = false + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1827,7 +1849,7 @@ MSG ensure_proper_type populate_with_current_scope_attributes - clear_timestamp_attributes + super end # Returns +true+ if the record is read only. Records loaded through joins with piggy-back @@ -2091,14 +2113,6 @@ MSG send("#{att}=", value) if respond_to?("#{att}=") end end - - # Clear attributes and changed_attributes - def clear_timestamp_attributes - all_timestamp_attributes_in_model.each do |attribute_name| - self[attribute_name] = nil - changed_attributes.delete(attribute_name) - end - end end Base.class_eval do @@ -2143,4 +2157,5 @@ MSG end end +require 'active_record/connection_adapters/abstract/connection_specification' ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) 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 61994d4a47..20863e73aa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -421,7 +421,7 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @connection_pools[klass.name] + pool = @connection_pools.delete(klass.name) return nil unless pool pool.automatic_reconnect = false diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 7312e34f01..c08c0263b9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -126,7 +126,7 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(self) + connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished end def retrieve_connection 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 2ae655e68d..dc4a53034b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -306,6 +306,16 @@ module ActiveRecord end end + # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work + # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in + # an UPDATE statement, so in the mysql adapters we redefine this to do that. + def join_to_update(update, select) #:nodoc: + subselect = select.clone + subselect.projections = [update.key] + + update.where update.key.in(subselect) + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 3de850ec9e..f93c7cd74a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -102,10 +102,13 @@ module ActiveRecord def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value - else - value - end.to_s(:db) + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value.to_s(:db) 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 077cf7df1b..443e61b527 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -4,20 +4,31 @@ require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_support/deprecation' -# TODO: Autoload these files -require 'active_record/connection_adapters/column' -require 'active_record/connection_adapters/abstract/schema_definitions' -require 'active_record/connection_adapters/abstract/schema_statements' -require 'active_record/connection_adapters/abstract/database_statements' -require 'active_record/connection_adapters/abstract/quoting' -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: + extend ActiveSupport::Autoload + + autoload :Column + + autoload_under 'abstract' do + autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions' + + autoload :SchemaStatements + autoload :DatabaseStatements + autoload :DatabaseLimits + autoload :Quoting + + autoload :ConnectionPool + autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' + autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' + autoload :ConnectionSpecification + + autoload :QueryCache + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb new file mode 100644 index 0000000000..4b7c74e0b8 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -0,0 +1,611 @@ +require 'active_support/core_ext/object/blank' + +module ActiveRecord + module ConnectionAdapters + class AbstractMysqlAdapter < AbstractAdapter + class Column < ConnectionAdapters::Column # :nodoc: + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? nil : '' + else + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + elsif missing_default_forged_as_empty_string?(default) + nil + else + super + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + # Must return the relevant concrete adapter + def adapter + raise NotImplementedError + end + + private + + def simplified_type(field_type) + return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") + + case field_type + when /enum/i, /set/i then :string + when /year/i then :integer + when /bit/i then :binary + else + super + end + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + ## + # :singleton-method: + # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation (which was the default + # behavior in versions 0.13.1 and earlier) you can add the following line + # to your application.rb file: + # + # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + class_attribute :emulate_booleans + self.emulate_booleans = true + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", + :string => { :name => "varchar", :limit => 255 }, + :text => { :name => "text" }, + :integer => { :name => "int", :limit => 4 }, + :float => { :name => "float" }, + :decimal => { :name => "decimal" }, + :datetime => { :name => "datetime" }, + :timestamp => { :name => "datetime" }, + :time => { :name => "time" }, + :date => { :name => "date" }, + :binary => { :name => "blob" }, + :boolean => { :name => "tinyint", :limit => 1 } + } + + # FIXME: Make the first parameter more similar for the two adapters + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + end + + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::MySQL.new(pool) + end + + def adapter_name #:nodoc: + self.class::ADAPTER_NAME + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + # Returns true, since this connection adapter supports savepoints. + def supports_savepoints? + true + end + + def supports_bulk_alter? #:nodoc: + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # HELPER METHODS =========================================== + + # The two drivers have slightly different ways of yielding hashes of results, so + # this method must be implemented to provide a uniform interface. + def each_hash(result) # :nodoc: + raise NotImplementedError + end + + # Overridden by the adapters to instantiate their specific Column type. + def new_column(field, default, type, null) # :nodoc: + Column.new(field, default, type, null) + end + + # Must return the Mysql error number from the exception, if the exception has an + # error number. + def error_number(exception) # :nodoc: + raise NotImplementedError + end + + # QUOTING ================================================== + + def quote(value, column = nil) + if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) + s = column.class.string_to_binary(value).unpack("H*")[0] + "x'#{s}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # DATABASE STATEMENTS ====================================== + + # Executes the SQL statement in the context of this connection. + def execute(sql, name = nil) + if name == :skip_logging + @connection.query(sql) + else + log(sql, name) { @connection.query(sql) } + end + rescue ActiveRecord::StatementInvalid => exception + if exception.message.split(":").first =~ /Packets out of order/ + raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." + else + raise + end + end + + # MysqlAdapter has to free a result after using it, so we use this method to write + # stuff in a abstract way without concerning ourselves about whether it needs to be + # explicitly freed or not. + def execute_and_free(sql, name = nil) #:nodoc: + yield execute(sql, name) + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE + # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support + # these, we must use a subquery. However, MySQL is too stupid to create a + # temporary table for this automatically, so we have to give it some prompting + # in the form of a subsubquery. Ugh! + def join_to_update(update, select) #:nodoc: + if select.limit || select.offset || select.orders.any? + subsubselect = select.clone + subsubselect.projections = [update.key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(update.key.name) + subselect.from subsubselect.as('__active_record_temp') + + update.where update.key.in(subselect) + else + update.table select.source + update.wheres = select.constraints + end + end + + # SCHEMA STATEMENTS ======================================== + + def structure_dump #:nodoc: + if supports_views? + sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" + else + sql = "SHOW TABLES" + end + + select_all(sql).map do |table| + table.delete('Table_type') + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" + end.join("") + end + + # Drops the database specified on the +name+ attribute + # and creates it again using the provided +options+. + def recreate_database(name, options = {}) + drop_database(name) + create_database(name, options) + end + + # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + # Drops a MySQL database. + # + # Example: + # drop_database('sebastian_development') + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil, database = nil) #:nodoc: + sql = ["SHOW TABLES", database].compact.join(' IN ') + + execute_and_free(sql, 'SCHEMA') do |result| + result.collect { |field| field.first } + end + end + + def table_exists?(name) + return true if super + + name = name.to_s + schema, table = name.split('.', 2) + + unless table # A table was provided without a schema + table = schema + schema = nil + end + + tables(nil, schema).include? table + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| + each_hash(result) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == 'PRIMARY' # skip the primary key + current_index = row[:Key_name] + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], []) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] + end + end + + indexes + end + + # Returns an array of +Column+ objects for the table specified by +table_name+. + def columns(table_name, name = nil)#:nodoc: + sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" + execute_and_free(sql, 'SCHEMA') do |result| + each_hash(result).map do |field| + new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") + end + end + end + + def create_table(table_name, options = {}) #:nodoc: + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def bulk_change_table(table_name, operations) #:nodoc: + sqls = operations.map do |command, args| + table, arguments = args.shift, args + method = :"#{command}_sql" + + if respond_to?(method) + send(method, table, *arguments) + else + raise "Unknown method called : #{method}(#{arguments.inspect})" + end + end.flatten.join(", ") + + execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") + end + + # Renames a table. + # + # Example: + # 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)}" + end + + def add_column(table_name, column_name, type, options = {}) + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + 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") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + end + + 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)}") + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def add_column_position!(sql, options) + if options[:first] + sql << " FIRST" + elsif options[:after] + sql << " AFTER #{quote_column_name(options[:after])}" + end + end + + # SHOW VARIABLES LIKE 'name' + def show_variable(name) + variables = select_all("SHOW VARIABLES LIKE '#{name}'") + variables.first['Value'] unless variables.empty? + end + + # Returns a table's primary key and belonging sequence. + def pk_and_sequence_for(table) + execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result| + keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] } + keys.length == 1 ? [keys.first, nil] : nil + end + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_modifier(node) + Arel::Nodes::Bin.new(node) + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + case error_number(exception) + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + end + + def add_column_sql(table_name, column_name, type, options = {}) + add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(add_column_sql, options) + add_column_position!(add_column_sql, options) + add_column_sql + end + + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + options[:null] = column.null + end + + change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" + add_column_options!(change_column_sql, options) + add_column_position!(change_column_sql, options) + change_column_sql + end + + def rename_column_sql(table_name, column_name, new_column_name) + options = {} + + if column = columns(table_name).find { |c| c.name == column_name.to_s } + options[:default] = column.default + options[:null] = column.null + else + raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" + end + + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] + rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" + add_column_options!(rename_column_sql, options) + rename_column_sql + end + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } + end + alias :remove_columns_sql :remove_column + + def add_index_sql(table_name, column_name, options = {}) + index_name, index_type, index_columns = add_index_options(table_name, column_name, options) + "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + end + + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "DROP INDEX #{index_name}" + end + + def add_timestamps_sql(table_name) + [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + end + + def remove_timestamps_sql(table_name) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end + + private + + def supports_views? + version[0] >= 5 + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 18fdfa29ec..8b574518e5 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +require 'active_record/connection_adapters/abstract_mysql_adapter' gem 'mysql2', '~> 0.3.6' require 'mysql2' @@ -20,191 +20,51 @@ module ActiveRecord end module ConnectionAdapters - class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: - end + class Mysql2Adapter < AbstractMysqlAdapter - class Mysql2Column < Column - BOOL = "tinyint(1)" - def extract_default(default) - if sql_type =~ /blob/i || type == :text - if default.blank? - return null ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) - nil - else - super + class Column < AbstractMysqlAdapter::Column # :nodoc: + def adapter + Mysql2Adapter end end - def has_default? - return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns - super - end - - private - def simplified_type(field_type) - return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end - end - - def extract_limit(sql_type) - case sql_type - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super - end - end - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - end - - class Mysql2Adapter < AbstractAdapter - cattr_accessor :emulate_booleans - self.emulate_booleans = true - ADAPTER_NAME = 'Mysql2' - PRIMARY = "PRIMARY" - - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } - } def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} + super configure_connection end - def self.visitor_for(pool) # :nodoc: - Arel::Visitors::MySQL.new(pool) - end - - def adapter_name - ADAPTER_NAME - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - def supports_primary_key? - true - end + # HELPER METHODS =========================================== - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - - def native_database_types - NATIVE_DATABASE_TYPES - end - - # QUOTING ================================================== - - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) - s = column.class.string_to_binary(value).unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def each_hash(result) # :nodoc: + if block_given? + result.each(:as => :hash, :symbolize_keys => true) do |row| + yield row + end else - super + to_enum(:each_hash, result) end end - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name}`" + def new_column(field, default, type, null) # :nodoc: + Column.new(field, default, type, null) end - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + def error_number(exception) + exception.error_number if exception.respond_to?(:error_number) end + # QUOTING ================================================== + def quote_string(string) @connection.escape(string) end - def quoted_true - QUOTED_TRUE - end - - def quoted_false - QUOTED_FALSE - end - def substitute_at(column, index) Arel.sql "\0" end - # REFERENTIAL INTEGRITY ==================================== - - def disable_referential_integrity(&block) #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") - - begin - update("SET FOREIGN_KEY_CHECKS = 0") - yield - ensure - update("SET FOREIGN_KEY_CHECKS = #{old}") - end - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -217,11 +77,6 @@ module ActiveRecord connect end - # this is set to true in 2.3, but we don't want it to be - def requires_reloading? - false - end - # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! @@ -277,17 +132,22 @@ module ActiveRecord # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - if name == :skip_logging - @connection.query(sql) - else - log(sql, name) { @connection.query(sql) } - end - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." - else - raise - end + + super + end + + def exec_query(sql, name = 'SQL', binds = []) + result = execute(sql, name) + ActiveRecord::Result.new(result.fields, result.to_a) + end + + alias exec_without_stmt exec_query + + # Returns an array of record hashes with the column names as keys and + # column values as values. + def select(sql, name = nil, binds = []) + binds = binds.dup + exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -316,358 +176,35 @@ module ActiveRecord @connection.last_id end - def update_sql(sql, name = nil) - super - @connection.affected_rows - end - - def begin_db_transaction - execute "BEGIN" - rescue Exception - # Transactions aren't supported - end - - def commit_db_transaction - execute "COMMIT" - rescue Exception - # Transactions aren't supported - end - - def rollback_db_transaction - execute "ROLLBACK" - rescue Exception - # Transactions aren't supported - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - # SCHEMA STATEMENTS ======================================== - - def structure_dump - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql).inject("") do |structure, table| - table.delete('Table_type') - structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" - end - end - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) - drop_database(name) - create_database(name, options) - end - - # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. - # - # Example: - # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' - # create_database 'matt_development' - # create_database 'matt_development', :charset => :big5 - def create_database(name, options = {}) - if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" - else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" - end - end - - # Drops a MySQL database. - # - # Example: - # drop_database('sebastian_development') - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" - end - - def current_database - select_value 'SELECT DATABASE() as db' - end - - # Returns the database character set. - def charset - show_variable 'character_set_database' - end - - # Returns the database collation strategy. - def collation - show_variable 'collation_database' - end - - def tables(name = nil, database = nil) #:nodoc: - sql = ["SHOW TABLES", database].compact.join(' IN ') - execute(sql, 'SCHEMA').collect do |field| - field.first - end - end - - def table_exists?(name) - return true if super - - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - tables(nil, schema).include? table - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - indexes = [] - current_index = nil - result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == PRIMARY # skip the primary key - current_index = row[:Key_name] - indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] - end - indexes - end - - # Returns an array of +Mysql2Column+ objects for the table specified by +table_name+. - def columns(table_name, name = nil) - sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" - columns = [] - result = execute(sql, 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) { |field| - columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") - } - columns - end - - def create_table(table_name, options = {}) - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) - end - - # Renames a table. - # - # Example: - # 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)}" - end - - def add_column(table_name, column_name, type, options = {}) - add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - execute(add_column_sql) - end - - def change_column_default(table_name, column_name, default) - column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, :default => default - end - - def change_column_null(table_name, column_name, null, default = nil) - column = column_for(table_name, column_name) - - 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") - end - - change_column table_name, column_name, column.sql_type, :null => null - end - - def change_column(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - execute(change_column_sql) - end - - def rename_column(table_name, column_name, new_column_name) - options = {} - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - execute(rename_column_sql) - end + private - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end + def connect + @connection = Mysql2::Client.new(@config) + configure_connection end - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end + def configure_connection + @connection.query_options.merge!(:as => :array) - # SHOW VARIABLES LIKE 'name'. - def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") - variables.first['Value'] unless variables.empty? - end + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - keys = [] - result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) do |row| - keys << row[:Field] if row[:Key] == "PRI" - end - keys.length == 1 ? [keys.first, nil] : nil - end + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if encoding - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end + # increase timeout so mysql server doesn't disconnect us + wait_timeout = @config[:wait_timeout] + wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + variable_assignments << "@@wait_timeout = #{wait_timeout}" - def case_sensitive_modifier(node) - Arel::Nodes::Bin.new(node) + execute("SET #{variable_assignments.join(', ')}", :skip_logging) end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end - - protected - def quoted_columns_for_index(column_names, options = {}) - length = options[:length] if options.is_a?(Hash) - - case length - when Hash - column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } - when Fixnum - column_names.map {|name| "#{quote_column_name(name)}(#{length})"} - else - column_names.map {|name| quote_column_name(name) } - end - end - - def translate_exception(exception, message) - return super unless exception.respond_to?(:error_number) - - case exception.error_number - when 1062 - RecordNotUnique.new(message, exception) - when 1452 - InvalidForeignKey.new(message, exception) - else - super - end - end - - private - def connect - @connection = Mysql2::Client.new(@config) - configure_connection - end - - def configure_connection - @connection.query_options.merge!(:as => :array) - - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variable_assignments = ['SQL_AUTO_IS_NULL=0'] - encoding = @config[:encoding] - - # make sure we set the encoding - variable_assignments << "NAMES '#{encoding}'" if encoding - - # increase timeout so mysql server doesn't disconnect us - wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) - variable_assignments << "@@wait_timeout = #{wait_timeout}" - - execute("SET #{variable_assignments.join(', ')}", :skip_logging) - end - - # Returns an array of record hashes with the column names as keys and - # column values as values. - def select(sql, name = nil, binds = []) - binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a - end - - def exec_query(sql, name = 'SQL', binds = []) - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - - log(sql, name, binds) do - begin - result = @connection.query(sql) - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." - else - raise - end - end - - ActiveRecord::Result.new(result.fields, result.to_a) - end - end - - def supports_views? - version[0] >= 5 - end - - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } - end - - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column - 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 14b950dbb0..73e3afe1d5 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -1,6 +1,6 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' -require 'set' +require 'active_record/connection_adapters/abstract_mysql_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'active_support/core_ext/hash/keys' gem 'mysql', '~> 2.8.1' require 'mysql' @@ -40,9 +40,29 @@ module ActiveRecord end module ConnectionAdapters - class MysqlColumn < Column #:nodoc: - class << self - def string_to_time(value) + # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with + # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). + # + # Options: + # + # * <tt>:host</tt> - Defaults to "localhost". + # * <tt>:port</tt> - Defaults to 3306. + # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". + # * <tt>:username</tt> - Defaults to "root" + # * <tt>:password</tt> - Defaults to nothing. + # * <tt>:database</tt> - The name of the database. No default, must be provided. + # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. + # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. + # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. + # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. + # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. + # + class MysqlAdapter < AbstractMysqlAdapter + + class Column < AbstractMysqlAdapter::Column #:nodoc: + def self.string_to_time(value) return super unless Mysql::Time === value new_time( value.year, @@ -54,234 +74,97 @@ module ActiveRecord value.second_part) end - def string_to_dummy_time(v) + def self.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) + def self.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? - return null ? nil : '' - else - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - elsif missing_default_forged_as_empty_string?(default) - nil - else - super + def adapter + MysqlAdapter end end - def has_default? - return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns - super - end + ADAPTER_NAME = 'MySQL' - private - def simplified_type(field_type) - return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - return :string if field_type =~ /enum/i + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max = 1000) super + @cache = {} end - def extract_limit(sql_type) - case sql_type - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super + def each(&block); @cache.each(&block); end + def key?(key); @cache.key?(key); end + def [](key); @cache[key]; end + def length; @cache.length; end + def delete(key); @cache.delete(key); end + + def []=(sql, key) + while @max <= @cache.size + @cache.shift.last[:stmt].close end + @cache[sql] = key end - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' + def clear + @cache.values.each do |hash| + hash[:stmt].close + end + @cache.clear end - end - - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractAdapter - - ## - # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line - # to your application.rb file: - # - # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false - cattr_accessor :emulate_booleans - self.emulate_booleans = true - - ADAPTER_NAME = 'MySQL' - - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } - } + end def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} - @statements = {} + super + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) @client_encoding = nil connect end - def self.visitor_for(pool) # :nodoc: - Arel::Visitors::MySQL.new(pool) - end - - def adapter_name #:nodoc: - ADAPTER_NAME - end - - def supports_bulk_alter? #:nodoc: - true - end - # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end + # HELPER METHODS =========================================== - # Returns true. - def supports_primary_key? #:nodoc: - true + def each_hash(result) # :nodoc: + if block_given? + result.each_hash do |row| + row.symbolize_keys! + yield row + end + else + to_enum(:each_hash, result) + end end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? #:nodoc: - true + def new_column(field, default, type, null) # :nodoc: + Column.new(field, default, type, null) end - def native_database_types #:nodoc: - NATIVE_DATABASE_TYPES + def error_number(exception) # :nodoc: + exception.errno if exception.respond_to?(:errno) end - # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary) - s = column.class.string_to_binary(value).unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") - else - super - end - end - def type_cast(value, column) return super unless value == true || value == false value ? 1 : 0 end - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name}`" - end - - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') - end - def quote_string(string) #:nodoc: @connection.quote(string) end - def quoted_true - QUOTED_TRUE - end - - def quoted_false - QUOTED_FALSE - end - - # REFERENTIAL INTEGRITY ==================================== - - def disable_referential_integrity #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") - - begin - update("SET FOREIGN_KEY_CHECKS = 0") - yield - ensure - update("SET FOREIGN_KEY_CHECKS = #{old}") - end - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -333,9 +216,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.values.each do |cache| - cache[:stmt].close - end @statements.clear end @@ -425,20 +305,11 @@ module ActiveRecord 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: - if name == :skip_logging - @connection.query(sql) - else - log(sql, name) { @connection.query(sql) } - end - rescue ActiveRecord::StatementInvalid => exception - if exception.message.split(":").first =~ /Packets out of order/ - raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings." - else - raise - end + def execute_and_free(sql, name = nil) + result = execute(sql, name) + ret = yield result + result.free + ret end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: @@ -447,11 +318,6 @@ module ActiveRecord end alias :create :insert_sql - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def exec_delete(sql, name, binds) log(sql, name, binds) do exec_stmt(sql, name, binds) do |cols, stmt| @@ -467,337 +333,8 @@ module ActiveRecord # Transactions aren't supported end - def commit_db_transaction #:nodoc: - execute "COMMIT" - rescue Exception - # Transactions aren't supported - end - - def rollback_db_transaction #:nodoc: - execute "ROLLBACK" - rescue Exception - # Transactions aren't supported - end - - def create_savepoint - execute("SAVEPOINT #{current_savepoint_name}") - end - - def rollback_to_savepoint - execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") - end - - def release_savepoint - execute("RELEASE SAVEPOINT #{current_savepoint_name}") - end - - # SCHEMA STATEMENTS ======================================== - - def structure_dump #:nodoc: - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql).map do |table| - table.delete('Table_type') - sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_without_stmt(sql).first['Create Table'] + ";\n\n" - end.join("") - end - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) #:nodoc: - drop_database(name) - create_database(name, options) - end - - # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. - # - # Example: - # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' - # create_database 'matt_development' - # create_database 'matt_development', :charset => :big5 - def create_database(name, options = {}) - if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" - else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" - end - end - - # Drops a MySQL database. - # - # Example: - # drop_database 'sebastian_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" - end - - def current_database - select_value 'SELECT DATABASE() as db' - end - - # Returns the database character set. - def charset - show_variable 'character_set_database' - end - - # Returns the database collation strategy. - def collation - show_variable 'collation_database' - end - - def tables(name = nil, database = nil) #:nodoc: - result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA') - tables = result.collect { |field| field[0] } - result.free - tables - end - - def table_exists?(name) - return true if super - - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - tables(nil, schema).include? table - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil)#:nodoc: - indexes = [] - current_index = nil - result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) - result.each do |row| - if current_index != row[2] - next if row[2] == "PRIMARY" # skip the primary key - current_index = row[2] - indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], []) - end - - indexes.last.columns << row[4] - indexes.last.lengths << row[7] - end - result.free - indexes - end - - # Returns an array of +MysqlColumn+ objects for the table specified by +table_name+. - def columns(table_name, name = nil)#:nodoc: - sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" - result = execute(sql, 'SCHEMA') - columns = result.collect { |field| MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } - result.free - columns - end - - def create_table(table_name, options = {}) #:nodoc: - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) - end - - # Renames a table. - # - # Example: - # 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)}" - end - - def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| - table, arguments = args.shift, args - method = :"#{command}_sql" - - if respond_to?(method) - send(method, table, *arguments) - else - raise "Unknown method called : #{method}(#{arguments.inspect})" - end - end.flatten.join(", ") - - execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") - end - - def add_column(table_name, column_name, type, options = {}) - execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") - end - - def change_column_default(table_name, column_name, default) #:nodoc: - column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, :default => default - end - - def change_column_null(table_name, column_name, null, default = nil) - column = column_for(table_name, column_name) - - 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") - end - - change_column table_name, column_name, column.sql_type, :null => null - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") - end - - 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)}") - end - - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end - end - - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end - - # SHOW VARIABLES LIKE 'name' - def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") - variables.first['Value'] unless variables.empty? - end - - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) #:nodoc: - keys = [] - result = execute("describe #{quote_table_name(table)}", 'SCHEMA') - result.each_hash do |h| - keys << h["Field"]if h["Key"] == "PRI" - end - result.free - keys.length == 1 ? [keys.first, nil] : nil - end - - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end - - def case_sensitive_modifier(node) - Arel::Nodes::Bin.new(node) - end - - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql - end - - protected - def quoted_columns_for_index(column_names, options = {}) - length = options[:length] if options.is_a?(Hash) - - case length - when Hash - column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } - when Fixnum - column_names.map {|name| "#{quote_column_name(name)}(#{length})"} - else - column_names.map {|name| quote_column_name(name) } - end - end - - def translate_exception(exception, message) - return super unless exception.respond_to?(:errno) - - case exception.errno - when 1062 - RecordNotUnique.new(message, exception) - when 1452 - InvalidForeignKey.new(message, exception) - else - super - end - end - - def add_column_sql(table_name, column_name, type, options = {}) - add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(add_column_sql, options) - add_column_position!(add_column_sql, options) - add_column_sql - end - - def remove_column_sql(table_name, *column_names) - columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } - end - alias :remove_columns_sql :remove_column - - def change_column_sql(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - add_column_options!(change_column_sql, options) - add_column_position!(change_column_sql, options) - change_column_sql - end - - def rename_column_sql(table_name, column_name, new_column_name) - options = {} - - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end - - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - rename_column_sql - end - - def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - "ADD #{index_type} INDEX #{index_name} (#{index_columns})" - end - - def remove_index_sql(table_name, options = {}) - index_name = index_name_for_remove(table_name, options) - "DROP INDEX #{index_name}" - end - - def add_timestamps_sql(table_name) - [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] - end - - def remove_timestamps_sql(table_name) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] - end - private + def exec_stmt(sql, name, binds) cache = {} if binds.empty? @@ -809,7 +346,6 @@ module ActiveRecord stmt = cache[:stmt] end - begin stmt.execute(*binds.map { |col, val| type_cast(val, col) }) rescue Mysql::Error => e @@ -838,59 +374,48 @@ module ActiveRecord result end - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] + def connect + encoding = @config[:encoding] + if encoding + @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil + end - @connection.real_connect(*@connection_options) + if @config[:sslca] || @config[:sslkey] + @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) + end - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) + @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] + @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] + @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - configure_connection - end + @connection.real_connect(*@connection_options) - def configure_connection - encoding = @config[:encoding] - execute("SET NAMES '#{encoding}'", :skip_logging) if encoding + # reconnect must be set after real_connect is called, because real_connect sets it to false internally + @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) - end + configure_connection + end - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(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 + def configure_connection + encoding = @config[:encoding] + execute("SET NAMES '#{encoding}'", :skip_logging) if encoding - def supports_views? - version[0] >= 5 - end + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + end - # Returns the version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } - end + def select(sql, name = nil, binds = []) + @connection.query_with_result = true + rows = exec_query(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 - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column - end + # Returns the version of the connected MySQL server. + def version + @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 45c13bdcd6..a09bf9c73f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/object/blank' +require 'active_record/connection_adapters/statement_pool' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' @@ -246,6 +247,43 @@ module ActiveRecord true end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @counter = 0 + @cache = {} + end + + def each(&block); @cache.each(&block); end + def key?(key); @cache.key?(key); end + def [](key); @cache[key]; end + def length; @cache.length; end + + def next_key + "a#{@counter + 1}" + end + + def []=(sql, key) + while @max <= @cache.size + dealloc(@cache.shift.last) + end + @counter += 1 + @cache[sql] = key + end + + def clear + @cache.each_value do |stmt_key| + dealloc stmt_key + end + @cache.clear + end + + private + def dealloc(key) + @connection.query "DEALLOCATE #{key}" + end + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -254,9 +292,10 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @table_alias_length = nil - @statements = {} connect + @statements = StatementPool.new @connection, + config.fetch(:statement_limit) { 1000 } if postgresql_version < 80200 raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" @@ -271,9 +310,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.each_value do |value| - @connection.query "DEALLOCATE #{value}" - end @statements.clear end @@ -954,6 +990,8 @@ module ActiveRecord end module Utils + extend self + # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. # +schema_name+ is nil if not specified in +name+. # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) @@ -964,7 +1002,7 @@ module ActiveRecord # * <tt>schema_name.table_name</tt> # * <tt>schema_name."table.name"</tt> # * <tt>"schema.name"."table name"</tt> - def self.extract_schema_and_table(name) + def extract_schema_and_table(name) table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse [schema, table] end @@ -994,7 +1032,7 @@ module ActiveRecord def exec_cache(sql, binds) unless @statements.key? sql - nextkey = "a#{@statements.length + 1}" + nextkey = @statements.next_key @connection.prepare nextkey, sql @statements[sql] = nextkey end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index c3a7b039ff..0a0da0b5d3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,4 +1,6 @@ require 'active_record/connection_adapters/sqlite_adapter' + +gem 'sqlite3', '~> 1.3.4' require 'sqlite3' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 486efc5ba0..7c7e762c19 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -1,4 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'active_support/core_ext/string/encoding' module ActiveRecord module ConnectionAdapters #:nodoc: @@ -47,9 +49,41 @@ module ActiveRecord end end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = {} + end + + def each(&block); @cache.each(&block); end + def key?(key); @cache.key?(key); end + def [](key); @cache[key]; end + def length; @cache.length; end + + def []=(sql, key) + while @max <= @cache.size + dealloc(@cache.shift.last[:stmt]) + end + @cache[sql] = key + end + + def clear + @cache.values.each do |hash| + dealloc hash[:stmt] + end + @cache.clear + end + + private + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + def initialize(connection, logger, config) super(connection, logger) - @statements = {} + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) @config = config end @@ -106,10 +140,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.values.map { |hash| hash[:stmt] }.each { |stmt| - stmt.close unless stmt.closed? - } - @statements.clear end @@ -148,7 +178,7 @@ module ActiveRecord end def quote_column_name(name) #:nodoc: - %Q("#{name}") + %Q("#{name.to_s.gsub('"', '""')}") end # Quote date/time values for use in SQL input. Includes microseconds @@ -161,10 +191,25 @@ module ActiveRecord end end - def type_cast(value, column) # :nodoc: - return super unless BigDecimal === value + if "<3".encoding_aware? + def type_cast(value, column) # :nodoc: + return value.to_f if BigDecimal === value + return super unless String === value + return super unless column && value - value.to_f + value = super + if column.type == :string && value.encoding == Encoding::ASCII_8BIT + @logger.error "Binary data inserted for `string` type on column `#{column.name}`" + value.encode! 'utf-8' + end + value + end + else + def type_cast(value, column) # :nodoc: + return super unless BigDecimal === value + + value.to_f + end end # DATABASE STATEMENTS ====================================== diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb new file mode 100644 index 0000000000..c6b1bc8b5b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module ConnectionAdapters + class StatementPool + include Enumerable + + def initialize(connection, max = 1000) + @connection = connection + @max = max + end + + def each + raise NotImplementedError + end + + def key?(key) + raise NotImplementedError + end + + def [](key) + raise NotImplementedError + end + + def length + raise NotImplementedError + end + + def []=(sql, key) + raise NotImplementedError + end + + def clear + raise NotImplementedError + end + + def delete(key) + raise NotImplementedError + end + end + end +end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3f36dcde14..6f1ec7f9b3 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -24,375 +24,368 @@ end class FixturesFileNotFound < StandardError; end -# Fixtures are a way of organizing data that you want to test against; in short, sample data. -# -# = Fixture formats -# -# Fixtures come in 1 flavor: -# -# 1. YAML fixtures -# -# == YAML fixtures -# -# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures -# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+. -# -# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed -# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is -# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). -# The fixture file ends with the <tt>.yml</tt> file extension (Rails example: -# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this: -# -# rubyonrails: -# id: 1 -# name: Ruby on Rails -# url: http://www.rubyonrails.org -# -# google: -# id: 2 -# name: Google -# url: http://www.google.com -# -# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an -# indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing -# pleasure. -# -# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. -# See http://yaml.org/type/omap.html -# for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table. -# This is commonly needed for tree structures. Example: -# -# --- !omap -# - parent: -# id: 1 -# parent_id: NULL -# title: Parent -# - child: -# id: 2 -# parent_id: 1 -# title: Child -# -# = Using fixtures in testcases -# -# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the -# fixtures, but first let's take a look at a sample unit test: -# -# require 'test_helper' -# -# class WebSiteTest < ActiveSupport::TestCase -# test "web_site_count" do -# assert_equal 2, WebSite.count -# end -# end -# -# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database, -# so this test will succeed. -# The testing environment will automatically load the all fixtures into the database before each test. -# To ensure consistent data, the environment deletes the fixtures before running the load. -# -# In addition to being available in the database, the fixture's data may also be accessed by -# using a special dynamic method, which has the same name as the model, and accepts the -# name of the fixture to instantiate: -# -# test "find" do -# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name -# end -# -# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests: -# -# test "find_alt_method_1" do -# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] -# end -# -# test "find_alt_method_2" do -# assert_equal "Ruby on Rails", @rubyonrails.news -# end -# -# In order to use these methods to access fixtured data within your testcases, you must specify one of the -# following in your <tt>ActiveSupport::TestCase</tt>-derived class: -# -# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) -# self.use_instantiated_fixtures = true -# -# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only) -# self.use_instantiated_fixtures = :no_instances -# -# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully -# traversed in the database to create the fixture hash and/or instance variables. This is expensive for -# large sets of fixtured data. -# -# = Dynamic fixtures with ERB -# -# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can -# mix ERB in with your YAML fixtures to create a bunch of fixtures for load testing, like: -# -# <% for i in 1..1000 %> -# fix_<%= i %>: -# id: <%= i %> -# name: guy_<%= 1 %> -# <% end %> -# -# This will create 1000 very simple YAML fixtures. -# -# Using ERB, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. -# This is however a feature to be used with some caution. The point of fixtures are that they're -# stable units of predictable sample data. If you feel that you need to inject dynamic values, then -# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values -# in fixtures are to be considered a code smell. -# -# = Transactional fixtures -# -# TestCases can use begin+rollback to isolate their changes to the database instead of having to -# delete+insert for every test case. -# -# class FooTest < ActiveSupport::TestCase -# self.use_transactional_fixtures = true -# -# test "godzilla" do -# assert !Foo.all.empty? -# Foo.destroy_all -# assert Foo.all.empty? -# end -# -# test "godzilla aftermath" do -# assert !Foo.all.empty? -# end -# end -# -# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, -# then you may omit all fixtures declarations in your test cases since all the data's already there -# and every case rolls back its changes. -# -# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide -# access to fixture data for every table that has been loaded through fixtures (depending on the -# value of +use_instantiated_fixtures+) -# -# When *not* to use transactional fixtures: -# -# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until -# all parent transactions commit, particularly, the fixtures transaction which is begun in setup -# and rolled back in teardown. Thus, you won't be able to verify -# the results of your transaction until Active Record supports nested transactions or savepoints (in progress). -# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. -# Use InnoDB, MaxDB, or NDB instead. -# -# = Advanced YAML Fixtures -# -# YAML fixtures that don't specify an ID get some extra features: -# -# * Stable, autogenerated IDs -# * Label references for associations (belongs_to, has_one, has_many) -# * HABTM associations as inline lists -# * Autofilled timestamp columns -# * Fixture label interpolation -# * Support for YAML defaults -# -# == Stable, autogenerated IDs -# -# Here, have a monkey fixture: -# -# george: -# id: 1 -# name: George the Monkey -# -# reginald: -# id: 2 -# name: Reginald the Pirate -# -# Each of these fixtures has two unique identifiers: one for the database -# and one for the humans. Why don't we generate the primary key instead? -# Hashing each fixture's label yields a consistent ID: -# -# george: # generated id: 503576764 -# name: George the Monkey -# -# reginald: # generated id: 324201669 -# name: Reginald the Pirate -# -# Active Record looks at the fixture's model class, discovers the correct -# primary key, and generates it right before inserting the fixture -# into the database. -# -# The generated ID for a given label is constant, so we can discover -# any fixture's ID without loading anything, as long as we know the label. -# -# == Label references for associations (belongs_to, has_one, has_many) -# -# Specifying foreign keys in fixtures can be very fragile, not to -# mention difficult to read. Since Active Record can figure out the ID of -# any fixture from its label, you can specify FK's by label instead of ID. -# -# === belongs_to -# -# Let's break out some more monkeys and pirates. -# -# ### in pirates.yml -# -# reginald: -# id: 1 -# name: Reginald the Pirate -# monkey_id: 1 -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# pirate_id: 1 -# -# Add a few more monkeys and pirates and break this into multiple files, -# and it gets pretty hard to keep track of what's going on. Let's -# use labels instead of IDs: -# -# ### in pirates.yml -# -# reginald: -# name: Reginald the Pirate -# monkey: george -# -# ### in monkeys.yml -# -# george: -# name: George the Monkey -# pirate: reginald -# -# Pow! All is made clear. Active Record reflects on the fixture's model class, -# finds all the +belongs_to+ associations, and allows you to specify -# a target *label* for the *association* (monkey: george) rather than -# a target *id* for the *FK* (<tt>monkey_id: 1</tt>). -# -# ==== Polymorphic belongs_to -# -# Supporting polymorphic relationships is a little bit more complicated, since -# Active Record needs to know what type your association is pointing at. Something -# like this should look familiar: -# -# ### in fruit.rb -# -# belongs_to :eater, :polymorphic => true -# -# ### in fruits.yml -# -# apple: -# id: 1 -# name: apple -# eater_id: 1 -# eater_type: Monkey -# -# Can we do better? You bet! -# -# apple: -# eater: george (Monkey) -# -# Just provide the polymorphic target type and Active Record will take care of the rest. -# -# === has_and_belongs_to_many -# -# Time to give our monkey some fruit. -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# -# ### in fruits.yml -# -# apple: -# id: 1 -# name: apple -# -# orange: -# id: 2 -# name: orange -# -# grape: -# id: 3 -# name: grape -# -# ### in fruits_monkeys.yml -# -# apple_george: -# fruit_id: 1 -# monkey_id: 1 -# -# orange_george: -# fruit_id: 2 -# monkey_id: 1 -# -# grape_george: -# fruit_id: 3 -# monkey_id: 1 -# -# Let's make the HABTM fixture go away. -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# fruits: apple, orange, grape -# -# ### in fruits.yml -# -# apple: -# name: apple -# -# orange: -# name: orange -# -# grape: -# name: grape -# -# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits -# on George's fixture, but we could've just as easily specified a list -# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on -# the fixture's model class and discovers the +has_and_belongs_to_many+ -# associations. -# -# == Autofilled timestamp columns -# -# If your table/model specifies any of Active Record's -# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+), -# they will automatically be set to <tt>Time.now</tt>. -# -# If you've set specific values, they'll be left alone. -# -# == Fixture label interpolation -# -# The label of the current fixture is always available as a column value: -# -# geeksomnia: -# name: Geeksomnia's Account -# subdomain: $LABEL -# -# Also, sometimes (like when porting older join table fixtures) you'll need -# to be able to get a hold of the identifier for a given label. ERB -# to the rescue: -# -# george_reginald: -# monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> -# pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> -# -# == Support for YAML defaults -# -# You probably already know how to use YAML to set and reuse defaults in -# your <tt>database.yml</tt> file. You can use the same technique in your fixtures: -# -# DEFAULTS: &DEFAULTS -# created_on: <%= 3.weeks.ago.to_s(:db) %> -# -# first: -# name: Smurf -# *DEFAULTS -# -# second: -# name: Fraggle -# *DEFAULTS -# -# Any fixture labeled "DEFAULTS" is safely ignored. - module ActiveRecord + # \Fixtures are a way of organizing data that you want to test against; in short, sample data. + # + # They are stored in YAML files, one file per model, which are placed in the directory + # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically + # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>). + # The fixture file ends with the <tt>.yml</tt> file extension (Rails example: + # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks + # like this: + # + # rubyonrails: + # id: 1 + # name: Ruby on Rails + # url: http://www.rubyonrails.org + # + # google: + # id: 2 + # name: Google + # url: http://www.google.com + # + # This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and + # is followed by an indented list of key/value pairs in the "key: value" format. Records are + # separated by a blank line for your viewing pleasure. + # + # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type. + # See http://yaml.org/type/omap.html + # for the specification. You will need ordered fixtures when you have foreign key constraints + # on keys in the same table. This is commonly needed for tree structures. Example: + # + # --- !omap + # - parent: + # id: 1 + # parent_id: NULL + # title: Parent + # - child: + # id: 2 + # parent_id: 1 + # title: Child + # + # = Using Fixtures in Test Cases + # + # Since fixtures are a testing construct, we use them in our unit and functional tests. There + # are two ways to use the fixtures, but first let's take a look at a sample unit test: + # + # require 'test_helper' + # + # class WebSiteTest < ActiveSupport::TestCase + # test "web_site_count" do + # assert_equal 2, WebSite.count + # end + # end + # + # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database, + # so this test will succeed. + # + # The testing environment will automatically load the all fixtures into the database before each + # test. To ensure consistent data, the environment deletes the fixtures before running the load. + # + # In addition to being available in the database, the fixture's data may also be accessed by + # using a special dynamic method, which has the same name as the model, and accepts the + # name of the fixture to instantiate: + # + # test "find" do + # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name + # end + # + # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the + # following tests: + # + # test "find_alt_method_1" do + # assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] + # end + # + # test "find_alt_method_2" do + # assert_equal "Ruby on Rails", @rubyonrails.news + # end + # + # In order to use these methods to access fixtured data within your testcases, you must specify one of the + # following in your <tt>ActiveSupport::TestCase</tt>-derived class: + # + # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) + # self.use_instantiated_fixtures = true + # + # - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only) + # self.use_instantiated_fixtures = :no_instances + # + # Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully + # traversed in the database to create the fixture hash and/or instance variables. This is expensive for + # large sets of fixtured data. + # + # = Dynamic fixtures with ERB + # + # Some times you don't care about the content of the fixtures as much as you care about the volume. + # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load + # testing, like: + # + # <% 1.upto(1000) do |i| %> + # fix_<%= i %>: + # id: <%= i %> + # name: guy_<%= 1 %> + # <% end %> + # + # This will create 1000 very simple fixtures. + # + # Using ERB, you can also inject dynamic values into your fixtures with inserts like + # <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. + # This is however a feature to be used with some caution. The point of fixtures are that they're + # stable units of predictable sample data. If you feel that you need to inject dynamic values, then + # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values + # in fixtures are to be considered a code smell. + # + # = Transactional Fixtures + # + # Test cases can use begin+rollback to isolate their changes to the database instead of having to + # delete+insert for every test case. + # + # class FooTest < ActiveSupport::TestCase + # self.use_transactional_fixtures = true + # + # test "godzilla" do + # assert !Foo.all.empty? + # Foo.destroy_all + # assert Foo.all.empty? + # end + # + # test "godzilla aftermath" do + # assert !Foo.all.empty? + # end + # end + # + # If you preload your test database with all fixture data (probably in the rake task) and use + # transactional fixtures, then you may omit all fixtures declarations in your test cases since + # all the data's already there and every case rolls back its changes. + # + # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to + # true. This will provide access to fixture data for every table that has been loaded through + # fixtures (depending on the value of +use_instantiated_fixtures+). + # + # When *not* to use transactional fixtures: + # + # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until + # all parent transactions commit, particularly, the fixtures transaction which is begun in setup + # and rolled back in teardown. Thus, you won't be able to verify + # the results of your transaction until Active Record supports nested transactions or savepoints (in progress). + # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. + # Use InnoDB, MaxDB, or NDB instead. + # + # = Advanced Fixtures + # + # Fixtures that don't specify an ID get some extra features: + # + # * Stable, autogenerated IDs + # * Label references for associations (belongs_to, has_one, has_many) + # * HABTM associations as inline lists + # * Autofilled timestamp columns + # * Fixture label interpolation + # * Support for YAML defaults + # + # == Stable, Autogenerated IDs + # + # Here, have a monkey fixture: + # + # george: + # id: 1 + # name: George the Monkey + # + # reginald: + # id: 2 + # name: Reginald the Pirate + # + # Each of these fixtures has two unique identifiers: one for the database + # and one for the humans. Why don't we generate the primary key instead? + # Hashing each fixture's label yields a consistent ID: + # + # george: # generated id: 503576764 + # name: George the Monkey + # + # reginald: # generated id: 324201669 + # name: Reginald the Pirate + # + # Active Record looks at the fixture's model class, discovers the correct + # primary key, and generates it right before inserting the fixture + # into the database. + # + # The generated ID for a given label is constant, so we can discover + # any fixture's ID without loading anything, as long as we know the label. + # + # == Label references for associations (belongs_to, has_one, has_many) + # + # Specifying foreign keys in fixtures can be very fragile, not to + # mention difficult to read. Since Active Record can figure out the ID of + # any fixture from its label, you can specify FK's by label instead of ID. + # + # === belongs_to + # + # Let's break out some more monkeys and pirates. + # + # ### in pirates.yml + # + # reginald: + # id: 1 + # name: Reginald the Pirate + # monkey_id: 1 + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # pirate_id: 1 + # + # Add a few more monkeys and pirates and break this into multiple files, + # and it gets pretty hard to keep track of what's going on. Let's + # use labels instead of IDs: + # + # ### in pirates.yml + # + # reginald: + # name: Reginald the Pirate + # monkey: george + # + # ### in monkeys.yml + # + # george: + # name: George the Monkey + # pirate: reginald + # + # Pow! All is made clear. Active Record reflects on the fixture's model class, + # finds all the +belongs_to+ associations, and allows you to specify + # a target *label* for the *association* (monkey: george) rather than + # a target *id* for the *FK* (<tt>monkey_id: 1</tt>). + # + # ==== Polymorphic belongs_to + # + # Supporting polymorphic relationships is a little bit more complicated, since + # Active Record needs to know what type your association is pointing at. Something + # like this should look familiar: + # + # ### in fruit.rb + # + # belongs_to :eater, :polymorphic => true + # + # ### in fruits.yml + # + # apple: + # id: 1 + # name: apple + # eater_id: 1 + # eater_type: Monkey + # + # Can we do better? You bet! + # + # apple: + # eater: george (Monkey) + # + # Just provide the polymorphic target type and Active Record will take care of the rest. + # + # === has_and_belongs_to_many + # + # Time to give our monkey some fruit. + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # + # ### in fruits.yml + # + # apple: + # id: 1 + # name: apple + # + # orange: + # id: 2 + # name: orange + # + # grape: + # id: 3 + # name: grape + # + # ### in fruits_monkeys.yml + # + # apple_george: + # fruit_id: 1 + # monkey_id: 1 + # + # orange_george: + # fruit_id: 2 + # monkey_id: 1 + # + # grape_george: + # fruit_id: 3 + # monkey_id: 1 + # + # Let's make the HABTM fixture go away. + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # fruits: apple, orange, grape + # + # ### in fruits.yml + # + # apple: + # name: apple + # + # orange: + # name: orange + # + # grape: + # name: grape + # + # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits + # on George's fixture, but we could've just as easily specified a list + # of monkeys on each fruit. As with +belongs_to+, Active Record reflects on + # the fixture's model class and discovers the +has_and_belongs_to_many+ + # associations. + # + # == Autofilled Timestamp Columns + # + # If your table/model specifies any of Active Record's + # standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+), + # they will automatically be set to <tt>Time.now</tt>. + # + # If you've set specific values, they'll be left alone. + # + # == Fixture label interpolation + # + # The label of the current fixture is always available as a column value: + # + # geeksomnia: + # name: Geeksomnia's Account + # subdomain: $LABEL + # + # Also, sometimes (like when porting older join table fixtures) you'll need + # to be able to get a hold of the identifier for a given label. ERB + # to the rescue: + # + # george_reginald: + # monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> + # pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> + # + # == Support for YAML defaults + # + # You probably already know how to use YAML to set and reuse defaults in + # your <tt>database.yml</tt> file. You can use the same technique in your fixtures: + # + # DEFAULTS: &DEFAULTS + # created_on: <%= 3.weeks.ago.to_s(:db) %> + # + # first: + # name: Smurf + # *DEFAULTS + # + # second: + # name: Fraggle + # *DEFAULTS + # + # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures MAX_ID = 2 ** 30 - 1 diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 04f494db2c..6bad36abb9 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -29,11 +29,21 @@ module ActiveRecord rows.each(&block) end + RESCUE_ERRORS = [ ArgumentError ] # :nodoc: + private + if defined?(Psych) && defined?(Psych::SyntaxError) + RESCUE_ERRORS << Psych::SyntaxError + end + def rows return @rows if @rows - data = YAML.load(render(IO.read(@file))) + begin + data = YAML.load(render(IO.read(@file))) + rescue *RESCUE_ERRORS => error + raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace + end @rows = data ? validate(data).to_a : [] end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 2eeff7e36f..ffee5a081a 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -71,7 +71,7 @@ module ActiveRecord end def invert_rename_index(args) - [:rename_index, args.reverse] + [:rename_index, [args.first] + args.last(2).reverse] end def invert_rename_column(args) diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 5a5351b517..fdf17c003c 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -111,7 +111,7 @@ module ActiveRecord callback_meth = :"_notify_#{observer_name}_for_#{callback}" unless klass.respond_to?(callback_meth) klass.send(:define_method, callback_meth) do |&block| - observer.send(callback, self, &block) + observer.update(callback, self, &block) end klass.send(callback, callback_meth) end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index e485901440..10c0dc6f2a 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -61,6 +61,12 @@ module ActiveRecord status, headers, body = @app.call(env) [status, headers, BodyProxy.new(old, body)] + rescue Exception => e + ActiveRecord::Base.connection.clear_query_cache + unless old + ActiveRecord::Base.connection.disable_query_cache! + end + raise e end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ec00f7faad..13c41350fb 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -94,7 +94,7 @@ db_namespace = namespace :db do "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" ActiveRecord::Base.establish_connection(config.merge( 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], creation_options) + ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) ActiveRecord::Base.connection.execute grant_statement ActiveRecord::Base.establish_connection(config) else diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2324039cf..89179779e3 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -226,7 +226,7 @@ module ActiveRecord if options[:counter_cache] == true "#{active_record.name.demodulize.underscore.pluralize}_count" elsif options[:counter_cache] - options[:counter_cache] + options[:counter_cache].to_s end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7e59eb4584..15fd1a58c8 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -216,14 +216,20 @@ module ActiveRecord if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else - stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) + stmt = Arel::UpdateManager.new(arel.engine) - if limit = arel.limit - stmt.take limit + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) + stmt.key = table[primary_key] + + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints end - stmt.order(*arel.orders) - stmt.key = table[primary_key] @klass.connection.update stmt, 'SQL', bind_values end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index ec1176e3dd..2fd89882ff 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -62,7 +62,7 @@ module ActiveRecord start = options.delete(:start).to_i batch_size = options.delete(:batch_size) || 1000 - relation = relation.except(:order).order(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(batch_size) records = relation.where(table[primary_key].gteq(start)).all while records.any? diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index d0d138d2a6..7eeb3dde70 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -114,7 +114,7 @@ module ActiveRecord def first(*args) if args.any? if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - to_a.first(*args) + limit(*args).to_a else apply_finder_options(args.first).first end @@ -134,7 +134,11 @@ module ActiveRecord def last(*args) if args.any? if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - to_a.last(*args) + if order_values.empty? && reorder_value.nil? + order("#{primary_key} DESC").limit(*args).reverse + else + to_a.last(*args) + end else apply_finder_options(args.first).last end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 2814771002..7e8ddd1b5d 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -19,7 +19,7 @@ module ActiveRecord case value when ActiveRecord::Relation - value.select_values = [value.klass.arel_table['id']] if value.select_values.empty? + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map { |x| diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1654ae1eac..7eda9ad8e8 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -254,12 +254,12 @@ module ActiveRecord association_joins = buckets['association_join'] || [] stashed_association_joins = buckets['stashed_join'] || [] - join_nodes = buckets['join_node'] || [] + join_nodes = (buckets['join_node'] || []).uniq string_joins = (buckets['string_join'] || []).map { |x| x.strip }.uniq - join_list = custom_join_ast(manager, string_joins) + join_list = join_nodes + custom_join_ast(manager, string_joins) join_dependency = ActiveRecord::Associations::JoinDependency.new( @klass, @@ -267,10 +267,6 @@ module ActiveRecord join_list ) - join_nodes.each do |join| - join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) - end - join_dependency.graft(*stashed_association_joins) @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? @@ -280,7 +276,6 @@ module ActiveRecord association.join_to(manager) end - manager.join_sources.concat join_nodes.uniq manager.join_sources.concat join_list manager diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 1511c71ffc..cccac6ffd7 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -37,6 +37,10 @@ module ActiveRecord self.record_timestamps = true end + def initialize_dup(other) + clear_timestamp_attributes + end + private def create #:nodoc: @@ -95,6 +99,13 @@ module ActiveRecord def current_time_from_proper_timezone #:nodoc: self.class.default_timezone == :utc ? Time.now.utc : Time.now end + + # Clear attributes and changed_attributes + def clear_timestamp_attributes + all_timestamp_attributes_in_model.each do |attribute_name| + self[attribute_name] = nil + changed_attributes.delete(attribute_name) + end + end end end - diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 509baacaef..94fc3564df 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -2,7 +2,7 @@ require "cases/helper" class ActiveSchemaTest < ActiveRecord::TestCase def setup - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do alias_method :execute_without_stub, :execute remove_method :execute def execute(sql, name = nil) return sql end @@ -10,7 +10,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def teardown - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do remove_method :execute alias_method :execute, :execute_without_stub end @@ -99,7 +99,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase private def with_real_execute #we need to actually modify some data, so we make execute point to the original method - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do alias_method :execute_with_stub, :execute remove_method :execute alias_method :execute, :execute_without_stub @@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase yield ensure #before finishing, we restore the alias to the mock-up method - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do remove_method :execute alias_method :execute, :execute_with_stub end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 3a7f1badf0..4c6d865d59 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -219,21 +219,6 @@ class SchemaTest < ActiveRecord::TestCase end end - def test_extract_schema_and_table - { - %(table_name) => [nil,'table_name'], - %("table.name") => [nil,'table.name'], - %(schema.table_name) => %w{schema table_name}, - %("schema".table_name) => %w{schema table_name}, - %(schema."table_name") => %w{schema table_name}, - %("schema"."table_name") => %w{schema table_name}, - %("even spaces".table) => ['even spaces','table'], - %(schema."table.name") => ['schema', 'table.name'] - }.each do |given, expect| - assert_equal expect, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils.extract_schema_and_table(given) - end - end - def test_current_schema { %('$user',public) => 'public', diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb new file mode 100644 index 0000000000..5f08f79171 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -0,0 +1,18 @@ +class PostgreSQLUtilsTest < ActiveSupport::TestCase + include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils + + def test_extract_schema_and_table + { + %(table_name) => [nil,'table_name'], + %("table.name") => [nil,'table.name'], + %(schema.table_name) => %w{schema table_name}, + %("schema".table_name) => %w{schema table_name}, + %(schema."table_name") => %w{schema table_name}, + %("schema"."table_name") => %w{schema table_name}, + %("even spaces".table) => ['even spaces','table'], + %(schema."table.name") => ['schema', 'table.name'] + }.each do |given, expect| + assert_equal expect, extract_schema_and_table(given) + end + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 6ff04e3eb3..2b598220ee 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,5 +1,6 @@ # encoding: utf-8 require "cases/helper" +require 'models/owner' module ActiveRecord module ConnectionAdapters @@ -19,6 +20,21 @@ module ActiveRecord eosql end + def test_column_types + return skip('only test encoding on 1.9') unless "<3".encoding_aware? + + owner = Owner.create!(:name => "hello".encode('ascii-8bit')) + owner.reload + select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', ' + result = Owner.connection.exec_query <<-esql + SELECT #{select} + FROM #{Owner.table_name} + WHERE #{Owner.primary_key} = #{owner.id} + esql + + assert(!result.rows.first.include?("blob"), "should not store blobs") + end + def test_exec_insert column = @conn.columns('items').find { |col| col.name == 'number' } vals = [[column, 10]] diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 818902beb5..866a3cca10 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -352,6 +352,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal members(:groucho), sponsor.sponsorable end + def test_dont_find_target_when_foreign_key_is_null + tagging = taggings(:thinking_general) + queries = assert_sql { tagging.super_tag } + assert_equal 0, queries.length + end + def test_field_name_same_as_foreign_key computer = Computer.find(1) assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # ' diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 24830a661a..8dc1423375 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -36,18 +36,32 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_marshalling_extensions + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + david = developers(:david) assert_equal projects(:action_controller), david.projects.find_most_recent - david = Marshal.load(Marshal.dump(david)) + marshalled = Marshal.dump(david) + david = Marshal.load(marshalled) + assert_equal projects(:action_controller), david.projects.find_most_recent end def test_marshalling_named_extensions + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + david = developers(:david) assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent - david = Marshal.load(Marshal.dump(david)) + marshalled = Marshal.dump(david) + david = Marshal.load(marshalled) + assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a2764f3e3b..1e59931963 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -17,6 +17,7 @@ require 'models/invoice' require 'models/line_item' require 'models/car' require 'models/bulb' +require 'models/engine' class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base @@ -850,6 +851,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_clearing_updates_counter_cache_when_inverse_counter_cache_is_a_symbol_with_dependent_destroy + car = Car.first + car.engines.create! + + assert_difference 'car.reload.engines_count', -1 do + car.engines.clear + end + end + def test_clearing_a_dependent_association_collection firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 0b1ba31ac2..b703c96ec1 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -813,4 +813,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert !c.save end end + + def test_preloading_empty_through_association_via_joins + person = Person.create!(:first_name => "Gaga") + person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').includes(:posts).to_a.first + + assert person.posts.loaded?, 'person.posts should be loaded' + assert_equal [], person.posts + end + + def test_explicitly_joining_join_table + assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet + end end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 80c6e41169..6122098623 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -356,6 +356,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal categories(:general), members(:groucho).club_category end + def test_has_one_through_has_one_through_with_belongs_to_source_reflection_with_default_scope_preload_on_the_through_model + prev_default_scope = Club.default_scopes + Club.default_scopes = [Club.preload(:category)] + + assert_equal categories(:general), members(:groucho).club_category + ensure + Club.default_scopes = prev_default_scope + end + def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) } general = categories(:general) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index c78d887ed7..87f5b5ee81 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -24,6 +24,7 @@ require 'models/joke' require 'models/bulb' require 'rexml/document' require 'active_support/core_ext/exception' +require 'bcrypt' class Category < ActiveRecord::Base; end class Categorization < ActiveRecord::Base; end @@ -67,6 +68,29 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_column_names_are_escaped + conn = ActiveRecord::Base.connection + classname = conn.class.name[/[^:]*$/] + badchar = { + 'SQLite3Adapter' => '"', + 'MysqlAdapter' => '`', + 'Mysql2Adapter' => '`', + 'PostgreSQLAdapter' => '"', + 'OracleAdapter' => '"', + }.fetch(classname) { + raise "need a bad char for #{classname}" + } + + quoted = conn.quote_column_name "foo#{badchar}bar" + if current_adapter?(:OracleAdapter) + # Oracle does not allow double quotes in table and column names at all + # therefore quoting removes them + assert_equal("#{badchar}foobar#{badchar}", quoted) + else + assert_equal("#{badchar}foo#{badchar * 2}bar#{badchar}", quoted) + end + end + def test_columns_should_obey_set_primary_key pk = Subscriber.columns.find { |x| x.name == 'nick' } assert pk.primary, 'nick should be primary key' @@ -1608,6 +1632,10 @@ class BasicsTest < ActiveRecord::TestCase assert !LooseDescendant.abstract_class? end + def test_abstract_class_table_name + assert_nil AbstractCompany.table_name + end + def test_base_class assert_equal LoosePerson, LoosePerson.base_class assert_equal LooseDescendant, LooseDescendant.base_class @@ -1807,21 +1835,41 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshal_round_trip + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + expected = posts(:welcome) - actual = Marshal.load(Marshal.dump(expected)) + marshalled = Marshal.dump(expected) + actual = Marshal.load(marshalled) assert_equal expected.attributes, actual.attributes end def test_marshal_new_record_round_trip - post = Marshal.load(Marshal.dump(Post.new)) + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + + marshalled = Marshal.dump(Post.new) + post = Marshal.load(marshalled) + assert post.new_record?, "should be a new record" end def test_marshalling_with_associations + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + post = Post.new post.comments.build - post = Marshal.load(Marshal.dump(post)) + + marshalled = Marshal.dump(post) + post = Marshal.load(marshalled) assert_equal 1, post.comments.length end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index a35baee4ed..660098b9ad 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -113,7 +113,27 @@ class EachTest < ActiveRecord::TestCase batch.map! { not_a_post } end end + end + def test_find_in_batches_should_ignore_the_order_default_scope + # First post is with title scope + first_post = PostWithDefaultScope.first + posts = [] + PostWithDefaultScope.find_in_batches do |batch| + posts.concat(batch) + end + # posts.first will be ordered using id only. Title order scope should not apply here + assert_not_equal first_post, posts.first + assert_equal posts(:welcome), posts.first + end + + def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.find_in_batches do |batch| + posts.concat(batch) + end + assert_equal special_posts_ids, posts.map(&:id) end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index d1dddd4c2c..14884e42af 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -58,68 +58,68 @@ module ActiveRecord if current_adapter?(:MysqlAdapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlColumn.new("title", "a", "binary(1)") + binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)") assert_equal "a", binary_column.default - varbinary_column = MysqlColumn.new("title", "a", "varbinary(1)") + varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - MysqlColumn.new("title", "a", "blob") + MysqlAdapter::Column.new("title", "a", "blob") end assert_raise ArgumentError do - MysqlColumn.new("title", "Hello", "text") + MysqlAdapter::Column.new("title", "Hello", "text") end - text_column = MysqlColumn.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, "text") assert_equal nil, text_column.default - not_null_text_column = MysqlColumn.new("title", nil, "text", false) + not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blog_and_test_data_types - blob_column = MysqlColumn.new("title", nil, "blob") + blob_column = MysqlAdapter::Column.new("title", nil, "blob") assert !blob_column.has_default? - text_column = MysqlColumn.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, "text") assert !text_column.has_default? end end if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Column.new("title", "a", "binary(1)") + binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)") assert_equal "a", binary_column.default - varbinary_column = Mysql2Column.new("title", "a", "varbinary(1)") + varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)") assert_equal "a", varbinary_column.default end def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - Mysql2Column.new("title", "a", "blob") + Mysql2Adapter::Column.new("title", "a", "blob") end assert_raise ArgumentError do - Mysql2Column.new("title", "Hello", "text") + Mysql2Adapter::Column.new("title", "Hello", "text") end - text_column = Mysql2Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, "text") assert_equal nil, text_column.default - not_null_text_column = Mysql2Column.new("title", nil, "text", false) + not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blog_and_test_data_types - blob_column = Mysql2Column.new("title", nil, "blob") + blob_column = Mysql2Adapter::Column.new("title", nil, "blob") assert !blob_column.has_default? - text_column = Mysql2Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, "text") assert !text_column.has_default? end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index abf317768f..bd0d161838 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -6,7 +6,12 @@ module ActiveRecord def setup @handler = ConnectionHandler.new @handler.establish_connection 'america', Base.connection_pool.spec - @klass = Struct.new(:name).new('america') + @klass = Class.new do + def self.name; 'america'; end + end + @subklass = Class.new(@klass) do + def self.name; 'north america'; end + end end def test_retrieve_connection @@ -28,6 +33,20 @@ module ActiveRecord def test_retrieve_connection_pool assert_not_nil @handler.retrieve_connection_pool(@klass) end + + def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection + assert_not_nil @handler.retrieve_connection_pool(@subklass) + end + + def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove + @handler.establish_connection 'north america', Base.connection_pool.spec + assert_not_same @handler.retrieve_connection_pool(@klass), + @handler.retrieve_connection_pool(@subklass) + + @handler.remove_connection @subklass + assert_same @handler.retrieve_connection_pool(@klass), + @handler.retrieve_connection_pool(@subklass) + end end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 4c6f8b9699..47bf2d007f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -252,6 +252,32 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 2/) { Topic.first(2).entries } + assert_sql(/LIMIT 5/) { Topic.last(5).entries } + end + + def test_last_with_integer_and_order_should_keep_the_order + assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) + end + + def test_last_with_integer_and_order_should_not_use_sql_limit + query = assert_sql { Topic.order("title").last(5).entries } + assert_equal 1, query.length + assert_no_match(/LIMIT/, query.first) + end + + def test_last_with_integer_and_reorder_should_not_use_sql_limit + query = assert_sql { Topic.reorder("title").last(5).entries } + assert_equal 1, query.length + assert_no_match(/LIMIT/, query.first) + end + + def test_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.first(5) + assert_kind_of Array, Topic.last(5) + end + def test_unexisting_record_exception_handling assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1).parent diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 913f6a3340..866dcefbab 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -20,6 +20,7 @@ require 'models/book' require 'models/admin' require 'models/admin/account' require 'models/admin/user' +require 'tempfile' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true @@ -45,6 +46,21 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_broken_yaml_exception + badyaml = Tempfile.new ['foo', '.yml'] + badyaml.write 'a: !ruby.yaml.org,2002:str |\nfoo' + badyaml.flush + + dir = File.dirname badyaml.path + name =File.basename badyaml.path, '.yml' + assert_raises(ActiveRecord::Fixture::FormatError) do + ActiveRecord::Fixtures.create_fixtures(dir, name) + end + ensure + badyaml.close + badyaml.unlink + end + def test_create_fixtures ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") assert Parrot.find_by_name('Curious George'), 'George is in the database' diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 643e949087..75e5dfa49b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -231,6 +231,18 @@ class LifecycleTest < ActiveRecord::TestCase assert_not_nil observer.topic_ids.last end + test "able to disable observers" do + observer = DeveloperObserver.instance # activate + observer.calls.clear + + ActiveRecord::Base.observers.disable DeveloperObserver do + Developer.create! :name => 'Ancestor', :salary => 100000 + SpecialDeveloper.create! :name => 'Descendent', :salary => 100000 + end + + assert_equal [], observer.calls + end + def test_observer_is_called_once observer = DeveloperObserver.instance # activate observer.calls.clear diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 36007255fa..d108b456f0 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -104,9 +104,9 @@ module ActiveRecord end def test_invert_rename_index - @recorder.record :rename_index, [:old, :new] + @recorder.record :rename_index, [:table, :old, :new] rename = @recorder.inverse.first - assert_equal [:rename_index, [:new, :old]], rename + assert_equal [:rename_index, [:table, :new, :old]], rename end def test_invert_add_timestamps diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index ed0240cada..4a09a87322 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -182,7 +182,7 @@ class NamedScopeTest < ActiveRecord::TestCase def test_first_and_last_should_allow_integers_for_limit assert_equal Topic.base.first(2), Topic.base.to_a.first(2) - assert_equal Topic.base.last(2), Topic.base.to_a.last(2) + assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2) end def test_first_and_last_should_not_use_query_when_results_are_loaded diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index ad17f6f83a..e3ad0cad90 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -13,6 +13,32 @@ class QueryCacheTest < ActiveRecord::TestCase ActiveRecord::Base.connection.disable_query_cache! end + def test_exceptional_middleware_clears_and_disables_cache_on_error + assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' + + mw = ActiveRecord::QueryCache.new lambda { |env| + Task.find 1 + Task.find 1 + assert_equal 1, ActiveRecord::Base.connection.query_cache.length + raise "lol borked" + } + assert_raises(RuntimeError) { mw.call({}) } + + assert_equal 0, ActiveRecord::Base.connection.query_cache.length + assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' + end + + def test_exceptional_middleware_leaves_enabled_cache_alone + ActiveRecord::Base.connection.enable_query_cache! + + mw = ActiveRecord::QueryCache.new lambda { |env| + raise "lol borked" + } + assert_raises(RuntimeError) { mw.call({}) } + + assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' + end + def test_middleware_delegates called = false mw = ActiveRecord::QueryCache.new lambda { |env| @@ -213,4 +239,4 @@ class QueryCacheBodyProxyTest < ActiveRecord::TestCase assert_equal proxy.to_path, "/path" end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 673aff403f..1e2093273e 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -524,4 +524,22 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count end + + def test_default_scope_is_threadsafe + if in_memory_db? + skip "in memory db can't share a db between threads" + end + + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + threads << Thread.new do + Thread.current[:long_default_scope] = true + assert_equal 1, ThreadsafeDeveloper.all.count + end + threads << Thread.new do + assert_equal 1, ThreadsafeDeveloper.all.count + end + threads.each(&:join) + end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 821da91f0a..da96afd718 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -540,6 +540,29 @@ class RelationTest < ActiveRecord::TestCase } end + def test_find_all_using_where_with_relation_and_alternate_primary_key + cool_first = minivans(:cool_first) + # switching the lines below would succeed in current rails + # assert_queries(2) { + assert_queries(1) { + relation = Minivan.where(:minivan_id => Minivan.where(:name => cool_first.name)) + assert_equal [cool_first], relation.all + } + end + + def test_find_all_using_where_with_relation_does_not_alter_select_values + david = authors(:david) + + subquery = Author.where(:id => david.id) + + assert_queries(1) { + relation = Author.where(:id => subquery) + assert_equal [david], relation.all + } + + assert_equal 0, subquery.select_values.size + end + def test_find_all_using_where_with_relation_with_joins david = authors(:david) assert_queries(1) { @@ -965,4 +988,42 @@ class RelationTest < ActiveRecord::TestCase def test_ordering_with_extra_spaces assert_equal authors(:david), Author.order('id DESC , name DESC').last end + + def test_update_all_with_joins + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id) + count = comments.count + + assert_equal count, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + end + + def test_update_all_with_joins_and_limit + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).limit(1) + assert_equal 1, comments.update_all(:post_id => posts(:thinking).id) + end + + def test_update_all_with_joins_and_limit_and_order + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('comments.id').limit(1) + assert_equal 1, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + assert_equal posts(:welcome), comments(:more_greetings).post + end + + def test_update_all_with_joins_and_offset + all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id) + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id) + end + + def test_update_all_with_joins_and_offset_and_order + all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('posts.id', 'comments.id') + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:more_greetings).post + assert_equal posts(:welcome), comments(:greetings).post + end end diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml index 01c95b3a4c..402ca85faf 100644 --- a/activerecord/test/fixtures/tasks.yml +++ b/activerecord/test/fixtures/tasks.yml @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html first_task: id: 1 starting: 2005-03-30t06:30:00.00+01:00 diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 76f20b1061..b9c2e8ec9a 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -8,7 +8,7 @@ class Car < ActiveRecord::Base has_one :frickinawesome_bulb, :class_name => "Bulb", :conditions => { :frickinawesome => true } has_many :tyres - has_many :engines + has_many :engines, :dependent => :destroy has_many :wheels, :as => :wheelable scope :incl_tyres, includes(:tyres) diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index f182a7fa97..4dc9fff9fd 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -227,3 +227,12 @@ class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base default_scope OpenStruct.new(:call => includes(:projects)) end + +class ThreadsafeDeveloper < ActiveRecord::Base + self.table_name = 'developers' + + def self.default_scope + sleep 0.05 if Thread.current[:long_default_scope] + limit(1) + end +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index affa37b02d..198a963cbc 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -171,4 +171,14 @@ class PostWithDefaultInclude < ActiveRecord::Base self.table_name = 'posts' default_scope includes(:comments) has_many :comments, :foreign_key => :post_id +end + +class PostWithDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + default_scope :order => :title +end + +class SpecialPostWithDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + default_scope where(:id => [1, 5,6]) end
\ No newline at end of file diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 79a88db0da..6c45e99671 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -1,4 +1,6 @@ class Toy < ActiveRecord::Base set_primary_key :toy_id belongs_to :pet + + scope :with_pet, joins(:pet) end |