diff options
Diffstat (limited to 'activerecord')
119 files changed, 3135 insertions, 1922 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e448609cf4..0f6a31d679 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,68 @@ *Rails 3.2.0 (unreleased)* +* Added ActiveRecord::Base.store for declaring simple single-column key/value stores [DHH] + + class User < ActiveRecord::Base + store :settings, accessors: [ :color, :homepage ] + end + + u = User.new(color: 'black', homepage: '37signals.com') + u.color # Accessor stored attribute + u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + + +* MySQL: case-insensitive uniqueness validation avoids calling LOWER when + the column already uses a case-insensitive collation. Fixes #561. + + [Joseph Palermo] + +* Transactional fixtures enlist all active database connections. You can test + models on different connections without disabling transactional fixtures. + + [Jeremy Kemper] + +* Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a + better approach over the old find_or_create_by dynamic methods because it's clearer which + arguments are used to find the record and which are used to create it: + + User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson") + + [Andrés Mejía] + +*Rails 3.1.1 (October 7, 2011)* + +* Add deprecation for the preload_associations method. Fixes #3022. + + [Jon Leighton] + +* Don't require a DB connection when loading a model that uses set_primary_key. GH #2807. + + [Jon Leighton] + +* Fix using select() with a habtm association, e.g. Person.friends.select(:name). GH #3030 and + #2923. + + [Hendy Tanata] + +* Fix belongs_to polymorphic with custom primary key on target. GH #3104. + + [Jon Leighton] + +* CollectionProxy#replace should change the DB records rather than just mutating the array. + Fixes #3020. + + [Jon Leighton] + +* LRU cache in mysql and sqlite are now per-process caches. + + * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache + keys are per process id. + * lib/active_record/connection_adapters/sqlite_adapter.rb: ditto + + [Aaron Patterson] + +* 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. @@ -22,7 +85,14 @@ a URI that specifies the connection configuration. For example: [Prem Sichanugrist] -*Rails 3.1.0 (unreleased)* + +*Rails 3.1.0 (August 30, 2011)* + +* Add a proxy_association method to association proxies, which can be called by association + extensions to access information about the association. This replaces proxy_owner etc with + proxy_association.owner. + + [Jon Leighton] * ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 822276589b..b5db57569c 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -197,13 +197,13 @@ 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 Source code can be downloaded as part of the Rails project on GitHub -* https://github.com/rails/rails/tree/master/activerecord/ +* https://github.com/rails/rails/tree/master/activerecord == License diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8ac4d9a225..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.1.3') + 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 59cf42a377..3572c640eb 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' @@ -41,6 +34,7 @@ module ActiveRecord eager_autoload do autoload :ActiveRecordError, 'active_record/errors' autoload :ConnectionNotEstablished, 'active_record/errors' + autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter' autoload :Aggregations autoload :Associations @@ -71,9 +65,11 @@ module ActiveRecord autoload :Persistence autoload :QueryCache autoload :Reflection + autoload :Result autoload :Schema autoload :SchemaDumper autoload :Serialization + autoload :Store autoload :SessionStore autoload :Timestamp autoload :Transactions diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 029d7a9b15..0952ea2829 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -460,6 +460,12 @@ module ActiveRecord # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. # + # However, inside the actual extension code, you will not have access to the <tt>record</tt> as + # above. In this case, you can access <tt>proxy_association</tt>. For example, + # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return + # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside + # association extensions. + # # === Association Join Models # # Has Many associations can be configured with the <tt>:through</tt> option to use an @@ -1081,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 @@ -1156,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 @@ -1319,7 +1329,7 @@ module ActiveRecord # # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but + # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ @@ -1565,7 +1575,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' + # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.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/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 9e6d9e73c5..6cc401e6cc 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -42,10 +42,6 @@ module ActiveRecord select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" end - if reflection.macro == :has_and_belongs_to_many - select_value ||= reflection.klass.arel_table[Arel.star] - end - select_value end @@ -68,7 +64,12 @@ module ActiveRecord end if reflection.source_macro == :belongs_to - key = reflection.association_primary_key + if reflection.options[:polymorphic] + key = reflection.association_primary_key(klass) + else + key = reflection.association_primary_key + end + foreign_key = reflection.foreign_key else key = reflection.foreign_key diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index adb6af7165..97f531d064 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 @@ -41,7 +45,11 @@ module ActiveRecord end def replace_keys(record) - owner[reflection.foreign_key] = record && record[reflection.association_primary_key] + if record + owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + else + owner[reflection.foreign_key] = nil + end end def foreign_key_present? 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/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 3e68f973e7..cec876149c 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -382,7 +382,7 @@ module ActiveRecord persisted.map! do |record| # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns - # record rather than memory.at(memory.index(record)). The behaviour is fixed in 1.9. + # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9. mem_index = memory.index(record) if mem_index diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 827dfb7ccb..3181ca9a32 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -46,7 +46,7 @@ module ActiveRecord delegate :select, :find, :first, :last, :build, :create, :create!, - :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq, + :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, :sum, :count, :size, :length, :empty?, :any?, :many?, :include?, :to => :@association @@ -58,23 +58,27 @@ module ActiveRecord alias_method :new, :build + def proxy_association + @association + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || - @association.klass.respond_to?(name, include_private) + proxy_association.klass.respond_to?(name, include_private) end def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) if match && match.instantiator? send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - @association.send :set_owner_attributes, r - @association.send :add_to_target, r + proxy_association.send :set_owner_attributes, r + proxy_association.send :add_to_target, r yield(r) if block_given? end end - if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method)) + if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) if load_target if target.respond_to?(method) target.send(method, *args, &block) @@ -104,7 +108,7 @@ module ActiveRecord alias_method :to_a, :to_ary def <<(*records) - @association.concat(records) && self + proxy_association.concat(records) && self end alias_method :push, :<< @@ -114,7 +118,7 @@ module ActiveRecord end def reload - @association.reload + proxy_association.reload self end end diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb index f7ce70db1a..1f917f58f2 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,7 +26,7 @@ module ActiveRecord join_table[reflection.association_foreign_key] => record.id ) - owner.connection.insert stmt.to_sql + owner.connection.insert stmt end record @@ -46,7 +46,7 @@ module ActiveRecord stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete - owner.connection.delete stmt.to_sql + owner.connection.delete stmt end 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/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb index 24be279449..b77b667219 100644 --- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb @@ -13,7 +13,7 @@ module ActiveRecord # access the aliased column on the join table def records_for(ids) scope = super - klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values) + klass.connection.select_all(scope.arel, 'SQL', scope.bind_values) end def owner_key_name diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 81172179e0..b347a94978 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, :joins, :eager_load) ) end scope diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d0687ed0b6..d7bfaa5655 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods @@ -11,56 +12,83 @@ module ActiveRecord # accessors, mutators and query methods. def define_attribute_methods return if attribute_methods_generated? - super(column_names) - @attribute_methods_generated = true + + if base_class == self + super(column_names) + @attribute_methods_generated = true + else + base_class.define_attribute_methods + end end def attribute_methods_generated? - @attribute_methods_generated ||= false + if base_class == self + @attribute_methods_generated ||= false + else + base_class.attribute_methods_generated? + end end def undefine_attribute_methods(*args) - super - @attribute_methods_generated = false + if base_class == self + super + @attribute_methods_generated = false + else + base_class.undefine_attribute_methods(*args) + end end - # Checks whether the method is defined in the model or any of its subclasses - # that also derive from Active Record. Raises DangerousAttributeError if the - # method is defined by Active Record though. def instance_method_already_implemented?(method_name) - method_name = method_name.to_s - index = ancestors.index(ActiveRecord::Base) || ancestors.length - @_defined_class_methods ||= ancestors.first(index).map { |m| - m.instance_methods(false) | m.private_instance_methods(false) - }.flatten.map {|m| m.to_s }.to_set + if dangerous_attribute_method?(method_name) + raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" + end - @@_defined_activerecord_methods ||= defined_activerecord_methods - raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) - @_defined_class_methods.include?(method_name) + super end - def defined_activerecord_methods + # A method name is 'dangerous' if it is already defined by Active Record, but + # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) + def dangerous_attribute_method?(method_name) active_record = ActiveRecord::Base - super_klass = ActiveRecord::Base.superclass - methods = (active_record.instance_methods - super_klass.instance_methods) + - (active_record.private_instance_methods - super_klass.private_instance_methods) - methods.map {|m| m.to_s }.to_set + superclass = ActiveRecord::Base.superclass + + (active_record.method_defined?(method_name) || + active_record.private_method_defined?(method_name)) && + !superclass.method_defined?(method_name) && + !superclass.private_method_defined?(method_name) end end - def method_missing(method_id, *args, &block) - # If we haven't generated any methods yet, generate them, then - # see if we've created the method we're looking for. - if !self.class.attribute_methods_generated? + # If we haven't generated any methods yet, generate them, then + # see if we've created the method we're looking for. + def method_missing(method, *args, &block) + unless self.class.attribute_methods_generated? self.class.define_attribute_methods - method_name = method_id.to_s - guard_private_attribute_method!(method_name, args) - send(method_id, *args, &block) + + if respond_to_without_attributes?(method) + send(method, *args, &block) + else + super + end else super end end + def attribute_missing(match, *args, &block) + if self.class.columns_hash[match.attr_name] + ActiveSupport::Deprecation.warn( + "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ + "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \ + "is a column of the table. If this error has happened through normal usage of Active " \ + "Record (rather than through your own code or external libraries), please report it as " \ + "a bug." + ) + end + + super + end + def respond_to?(name, include_private = false) self.class.define_attribute_methods unless self.class.attribute_methods_generated? super diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 8bd898d126..a404a5edd7 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -3,11 +3,10 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array or nil if - # the record is not persisted? or has just been destroyed. + # Returns this record's primary key value wrapped in an Array if one is available def to_key key = send(self.class.primary_key) - persisted? && key ? [key] : nil + [key] if key end module ClassMethods @@ -67,7 +66,6 @@ module ActiveRecord @primary_key ||= '' self.original_primary_key = @primary_key value &&= value.to_s - connection_pool.primary_keys[table_name] = value self.primary_key = block_given? ? instance_eval(&block) : value end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 9a50a20fbc..4174e4da09 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -6,8 +6,6 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - attribute_method_suffix "" - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c77a3ac145..e9cdb130db 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,6 +17,10 @@ module ActiveRecord write_attribute(attr_name, new_value) end end + + if attr_name == primary_key && attr_name != "id" + generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='") + end end end @@ -24,12 +28,16 @@ module ActiveRecord # for fixnum and float columns are turned into +nil+. def write_attribute(attr_name, value) attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key @attributes_cache.delete(attr_name) - if (column = column_for_attribute(attr_name)) && column.number? + column = column_for_attribute(attr_name) + + if column && column.number? @attributes[attr_name] = convert_number_column_value(value) - else + elsif column || @attributes.has_key?(attr_name) @attributes[attr_name] = value + else + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end end alias_method :raw_write_attribute, :write_attribute diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 085fdba639..056170d82a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -370,7 +370,10 @@ module ActiveRecord else key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave) - record[reflection.foreign_key] = key + unless reflection.through_reflection + record[reflection.foreign_key] = key + end + saved = record.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c26119287a..6dae90266e 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 @@ -438,6 +442,7 @@ module ActiveRecord #:nodoc: class << self # Class methods delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped @@ -504,8 +509,7 @@ module ActiveRecord #:nodoc: if attributes.is_a?(Array) attributes.collect { |attr| create(attr, options, &block) } else - object = new(attributes, options) - yield(object) if block_given? + object = new(attributes, options, &block) object.save object end @@ -621,6 +625,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 @@ -701,6 +707,10 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns + if defined?(@primary_key) + connection_pool.primary_keys[table_name] ||= primary_key + end + connection_pool.columns[table_name] end @@ -938,17 +948,6 @@ module ActiveRecord #:nodoc: self.current_scope = nil end - # Specifies how the record is loaded by +Marshal+. - # - # +_load+ sets an instance variable for each key in the hash it takes as input. - # Override this method if you require more complex marshalling. - def _load(data) - record = allocate - record.init_with(Marshal.load(data)) - record - end - - # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. @@ -1268,27 +1267,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 @@ -1321,7 +1336,7 @@ MSG # Returns the class descending directly from ActiveRecord::Base or an # abstract class, if any, in the inheritance hierarchy. def class_of_active_record_descendant(klass) - if klass.superclass == Base || klass.superclass.abstract_class? + if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? klass elsif klass.superclass.nil? raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" @@ -1410,9 +1425,8 @@ MSG attrs = expand_hash_conditions_for_aggregates(attrs) table = Arel::Table.new(table_name).alias(default_table_name) - viz = Arel::Visitors.for(arel_engine) PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| - viz.accept b + connection.visitor.accept b }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -1590,16 +1604,6 @@ MSG self end - # Specifies how the record is dumped by +Marshal+. - # - # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this - # method if you require more complex marshalling. - def _dump(level) - dump = {} - encode_with(dump) - Marshal.dump(dump) - end - # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -1850,7 +1854,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 @@ -2114,14 +2118,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 @@ -2149,7 +2145,7 @@ MSG # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization + include Aggregations, Transactions, Reflection, Serialization, Store NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) @@ -2166,6 +2162,5 @@ MSG end end -# TODO: Remove this and make it work with LAZY flag -require 'active_record/connection_adapters/abstract_adapter' +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 ddfdb05297..77a5fe1efb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -82,10 +82,11 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - @connections = [] - @checked_out = [] + @connections = [] + @checked_out = [] @automatic_reconnect = true - @tables = {} + @tables = {} + @visitor = nil @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| @@ -298,12 +299,22 @@ module ActiveRecord :connected?, :disconnect!, :with => :@connection_mutex private + def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + connection = ActiveRecord::Base.send(spec.adapter_method, spec.config) + + # TODO: This is a bit icky, and in the long term we may want to change the method + # signature for connections. Also, if we switch to have one visitor per + # connection (and therefore per thread), we can get rid of the thread-local + # variable in Arel::Visitors::ToSql. + @visitor ||= connection.class.visitor_for(self) + connection.visitor = @visitor + + connection end def current_connection_id #:nodoc: - Thread.current.object_id + ActiveRecord::Base.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -410,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 123b3654e6..3d0f146fed 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -54,7 +54,7 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError # may be returned on an error. - def self.establish_connection(spec = nil) + def self.establish_connection(spec = ENV["DATABASE_URL"]) case spec when nil raise AdapterNotSpecified unless defined?(Rails.env) @@ -115,6 +115,14 @@ module ActiveRecord retrieve_connection end + def connection_id + Thread.current['ActiveRecord::Base.connection_id'] + end + + def connection_id=(connection_id) + Thread.current['ActiveRecord::Base.connection_id'] = connection_id + end + # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config @@ -126,7 +134,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 777ef15dfc..dc4a53034b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,30 +1,39 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + # Converts an arel AST to SQL + def to_sql(arel) + if arel.respond_to?(:ast) + visitor.accept(arel.ast) + else + arel + end + end + # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil, binds = []) - select(sql, name, binds) + def select_all(arel, name = nil, binds = []) + select(to_sql(arel), name, binds) end # Returns a record hash with the column names as keys and column values # as values. - def select_one(sql, name = nil) - result = select_all(sql, name) + def select_one(arel, name = nil) + result = select_all(arel, name) result.first if result end # Returns a single value from a record - def select_value(sql, name = nil) - if result = select_one(sql, name) + def select_value(arel, name = nil) + if result = select_one(arel, name) result.values.first end end # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(sql, name = nil) - result = select_rows(sql, name) + def select_values(arel, name = nil) + result = select_rows(to_sql(arel), name) result.map { |v| v[0] } end @@ -74,20 +83,20 @@ module ActiveRecord # # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds) + def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) + sql, binds = sql_for_insert(to_sql(arel), pk, id_value, sequence_name, binds) value = exec_insert(sql, name, binds) id_value || last_inserted_id(value) end # Executes the update statement and returns the number of rows affected. - def update(sql, name = nil, binds = []) - exec_update(sql, name, binds) + def update(arel, name = nil, binds = []) + exec_update(to_sql(arel), name, binds) end # Executes the delete statement and returns the number of rows affected. - def delete(sql, name = nil, binds = []) - exec_delete(sql, name, binds) + def delete(arel, name = nil, binds = []) + exec_delete(to_sql(arel), name, binds) end # Checks whether there is currently no transaction active. This is done @@ -297,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/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 093c30aa42..27ff13ad89 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -55,9 +55,10 @@ module ActiveRecord @query_cache.clear end - def select_all(sql, name = nil, binds = []) + def select_all(arel, name = nil, binds = []) if @query_cache_enabled - cache_sql(sql, binds) { super } + sql = to_sql(arel) + cache_sql(sql, binds) { super(sql, name, binds) } else super end 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 65024d76f8..4c3a8f7233 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -2,21 +2,33 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' - -# 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' +require 'active_support/deprecation' 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 @@ -38,12 +50,34 @@ module ActiveRecord define_callbacks :checkout, :checkin + attr_accessor :visitor + def initialize(connection, logger = nil) #:nodoc: @active = nil @connection, @logger = connection, logger @query_cache_enabled = false @query_cache = Hash.new { |h,sql| h[sql] = {} } + @open_transactions = 0 @instrumenter = ActiveSupport::Notifications.instrumenter + @visitor = nil + end + + # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface + def self.visitor_for(pool) # :nodoc: + adapter = pool.spec.config[:adapter] + + if Arel::Visitors::VISITORS[adapter] + ActiveSupport::Deprecation.warn( + "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \ + "should define a visitor_for method which returns the appropriate visitor for " \ + "the database. For example, MysqlAdapter.visitor_for(pool) returns " \ + "Arel::Visitors::MySQL.new(pool)." + ) + + Arel::Visitors::VISITORS[adapter].new(pool) + else + Arel::Visitors::ToSql.new(pool) + end end # Returns the human-readable name of the adapter. Use mixed case - one @@ -177,12 +211,9 @@ module ActiveRecord @connection end - def open_transactions - @open_transactions ||= 0 - end + attr_reader :open_transactions def increment_open_transactions - @open_transactions ||= 0 @open_transactions += 1 end @@ -207,6 +238,10 @@ module ActiveRecord node end + def case_insensitive_comparison(table, attribute, column, value) + table[attribute].lower.eq(table.lower(value)) + end + def current_savepoint_name "active_record_#{open_transactions}" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb new file mode 100644 index 0000000000..dd573ba569 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -0,0 +1,630 @@ +require 'active_support/core_ext/object/blank' + +module ActiveRecord + module ConnectionAdapters + class AbstractMysqlAdapter < AbstractAdapter + class Column < ConnectionAdapters::Column # :nodoc: + attr_reader :collation + + def initialize(name, default, sql_type = nil, null = true, collation = nil) + super(name, default, sql_type, null) + @collation = collation + 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 + 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 + + def case_sensitive? + collation && !collation.match(/_ci$/) + 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, collation) # :nodoc: + Column.new(field, default, type, null, collation) + 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 FULL 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", field[:Collation]) + 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 case_insensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + super + else + table[attribute].eq(value) + end + 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 d6c167ad36..971f3c35f3 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,187 +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 adapter_name - ADAPTER_NAME - end + # HELPER METHODS =========================================== - # 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 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, collation) # :nodoc: + Column.new(field, default, type, null, collation) 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? @@ -213,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! @@ -273,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) @@ -312,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 + private - 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) + def connect + @connection = Mysql2::Client.new(@config) + configure_connection 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 configure_connection + @connection.query_options.merge!(:as => :array) - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - 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] - # SHOW VARIABLES LIKE 'name'. - def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") - variables.first['Value'] unless variables.empty? - end + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if 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 - - # 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) - - quoted_column_names = 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 9e6cb13cca..f092edecda 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,230 +74,102 @@ 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 = Hash.new { |h,pid| h[pid] = {} } 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 } - } + private + def cache + @cache[$$] + end + 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 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, collation) # :nodoc: + Column.new(field, default, type, null, collation) 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? @@ -329,9 +221,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.values.each do |cache| - cache[:stmt].close - end @statements.clear end @@ -421,20 +310,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: @@ -443,11 +323,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| @@ -463,337 +338,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? @@ -805,7 +351,6 @@ module ActiveRecord stmt = cache[:stmt] end - begin stmt.execute(*binds.map { |col, val| type_cast(val, col) }) rescue Mysql::Error => e @@ -834,59 +379,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 a84f73c73f..d859843260 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,47 @@ module ActiveRecord true end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @counter = 0 + @cache = Hash.new { |h,pid| h[pid] = {} } + 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 cache + @cache[$$] + end + + 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 +296,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!" @@ -265,11 +308,12 @@ module ActiveRecord @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::PostgreSQL.new(pool) + end + # Clears the prepared statements cache. def clear_cache! - @statements.each_value do |value| - @connection.query "DEALLOCATE #{value}" - end @statements.clear end @@ -614,9 +658,11 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def recreate_database(name) #:nodoc: + # 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) + create_database(name, options) end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, @@ -677,12 +723,12 @@ module ActiveRecord binds << [nil, schema] if schema exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind in ('v','r') - AND c.relname = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} + SELECT COUNT(*) + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind in ('v','r') + AND c.relname = $1 + AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} SQL end @@ -948,6 +994,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+) @@ -958,7 +1006,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 @@ -987,13 +1035,14 @@ module ActiveRecord end def exec_cache(sql, binds) - unless @statements.key? sql - nextkey = "a#{@statements.length + 1}" + sql_key = "#{schema_search_path}-#{sql}" + unless @statements.key? sql_key + nextkey = @statements.next_key @connection.prepare nextkey, sql - @statements[sql] = nextkey + @statements[sql_key] = nextkey end - key = @statements[sql] + key = @statements[sql_key] # Clear the queue @connection.get_last_result 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 e2a0f63393..1932a849ee 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,12 +49,52 @@ module ActiveRecord end end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + 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 cache + @cache[$$] + end + + 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 + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::SQLite.new(pool) + end + def adapter_name #:nodoc: 'SQLite' end @@ -102,10 +144,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 @@ -144,7 +182,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 @@ -157,10 +195,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 = 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 + value.to_f + end end # DATABASE STATEMENTS ====================================== @@ -238,15 +291,15 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - @connection.transaction + log('begin transaction',nil) { @connection.transaction } end def commit_db_transaction #:nodoc: - @connection.commit + log('commit transaction',nil) { @connection.commit } end def rollback_db_transaction #:nodoc: - @connection.rollback + log('rollback transaction',nil) { @connection.rollback } end # SCHEMA 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/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 4d387565d9..3c7defedac 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -33,7 +33,7 @@ module ActiveRecord stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ arel_table[counter_name] => object.send(association).count }) - connection.update stmt.to_sql + connection.update stmt end return true end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index ad7d8cd63c..96870cb338 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -169,4 +169,17 @@ module ActiveRecord @errors = errors end end + + # Raised when a primary key is needed, but there is not one specified in the schema or model. + class UnknownPrimaryKey < ActiveRecordError + attr_reader :model + + def initialize(model) + @model = model + end + + def message + "Unknown primary key for table #{model.table_name} in model #{model}." + end + end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3f36dcde14..cad9417216 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 @@ -849,9 +842,12 @@ module ActiveRecord @loaded_fixtures = load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.connection.increment_open_transactions - ActiveRecord::Base.connection.transaction_joinable = false - ActiveRecord::Base.connection.begin_db_transaction + @fixture_connections = enlist_fixture_connections + @fixture_connections.each do |connection| + connection.increment_open_transactions + connection.transaction_joinable = false + connection.begin_db_transaction + end # Load fixtures for every test. else ActiveRecord::Fixtures.reset_cache @@ -871,13 +867,22 @@ module ActiveRecord end # Rollback changes if a transaction is active. - if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0 - ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions + if run_in_transaction? + @fixture_connections.each do |connection| + if connection.open_transactions != 0 + connection.rollback_db_transaction + connection.decrement_open_transactions + end + end + @fixture_connections.clear end ActiveRecord::Base.clear_active_connections! end + def enlist_fixture_connections + ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection) + end + private def load_fixtures fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) 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/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 6cfce6e573..d9ad7e4132 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -70,7 +70,7 @@ module ActiveRecord # If the locking column has no default value set, # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as + # <tt>locking_enabled?</tt> at this point as # <tt>@attributes</tt> may not have been initialized yet. if result.key?(self.class.locking_column) && lock_optimistically @@ -100,7 +100,7 @@ module ActiveRecord ) ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) - affected_rows = connection.update stmt.to_sql + affected_rows = connection.update stmt unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 507f345ef5..7166f1b82a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -332,6 +332,10 @@ module ActiveRecord (delegate || superclass.delegate).send(name, *args, &block) end + def self.migrate(direction) + new.migrate direction + end + cattr_accessor :verbose attr_accessor :name, :version @@ -559,7 +563,7 @@ module ActiveRecord def get_all_versions table = Arel::Table.new(schema_migrations_table_name) - Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort + Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort end def current_version @@ -716,11 +720,11 @@ module ActiveRecord if down? @migrated_versions.delete(version) stmt = table.where(table["version"].eq(version.to_s)).compile_delete - Base.connection.delete stmt.to_sql + Base.connection.delete stmt else @migrated_versions.push(version).sort! stmt = table.compile_insert table["version"] => version.to_s - Base.connection.insert stmt.to_sql + Base.connection.insert stmt end 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/persistence.rb b/activerecord/lib/active_record/persistence.rb index d9cd7987b0..5e65e46a7d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -287,7 +287,7 @@ module ActiveRecord private - # A hook to be overriden by association modules. + # A hook to be overridden by association modules. def destroy_associations end @@ -304,7 +304,7 @@ module ActiveRecord return 0 if attributes_with_values.empty? klass = self.class stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) - klass.connection.update stmt.to_sql + klass.connection.update stmt end # Creates a record with values matching those of the instance attributes @@ -314,7 +314,7 @@ module ActiveRecord new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id + self.id ||= new_id if self.class.primary_key IdentityMap.add(self) if IdentityMap.enabled? @new_record = false diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index e485901440..466d148901 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -28,9 +28,10 @@ module ActiveRecord end class BodyProxy # :nodoc: - def initialize(original_cache_value, target) + def initialize(original_cache_value, target, connection_id) @original_cache_value = original_cache_value @target = target + @connection_id = connection_id end def method_missing(method_sym, *arguments, &block) @@ -48,6 +49,7 @@ module ActiveRecord def close @target.close if @target.respond_to?(:close) ensure + ActiveRecord::Base.connection_id = @connection_id ActiveRecord::Base.connection.clear_query_cache unless @original_cache_value ActiveRecord::Base.connection.disable_query_cache! @@ -60,7 +62,13 @@ module ActiveRecord ActiveRecord::Base.connection.enable_query_cache! status, headers, body = @app.call(env) - [status, headers, BodyProxy.new(old, body)] + [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)] + 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 0ee7e20cf1..4fb19b14ea 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 @@ -203,11 +203,13 @@ db_namespace = namespace :db do end db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") file_list = [] - Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| - # only files matching "20091231235959_some_name.rb" pattern - if match_data = /^(\d{14})_(.+)\.rb$/.match(file) - status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2].humanize] + ActiveRecord::Migrator.migrations_paths.each do |path| + Dir.foreach(path) do |file| + # only files matching "20091231235959_some_name.rb" pattern + if match_data = /^(\d{14})_(.+)\.rb$/.match(file) + status = db_list.delete(match_data[1]) ? 'up' : 'down' + file_list << [status, match_data[1], match_data[2].humanize] + end end end db_list.map! do |version| @@ -286,7 +288,7 @@ db_namespace = namespace :db do pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end - abort %{Run "rake db:migrate" to update your database then try again.} + abort %{Run `rake db:migrate` to update your database then try again.} end end end @@ -339,9 +341,10 @@ db_namespace = namespace :db do namespace :schema do desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR' - task :dump => :load_config do + task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' - File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file| + filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" + File.open(filename, "w:utf-8") do |file| ActiveRecord::Base.establish_connection(Rails.env) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end @@ -354,7 +357,7 @@ db_namespace = namespace :db do if File.exists?(file) load(file) else - abort %{#{file} doesn't exist yet. Run "rake db:migrate" to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} + abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded} end end end @@ -421,10 +424,10 @@ db_namespace = namespace :db do ENV['PGHOST'] = abcs['test']['host'] if abcs['test']['host'] ENV['PGPORT'] = abcs['test']['port'].to_s if abcs['test']['port'] ENV['PGPASSWORD'] = abcs['test']['password'].to_s if abcs['test']['password'] - `psql -U "#{abcs['test']['username']}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs['test']['database']} #{abcs['test']['template']}` + `psql -U "#{abcs['test']['username']}" -f "#{Rails.root}/db/#{Rails.env}_structure.sql" #{abcs['test']['database']} #{abcs['test']['template']}` when /sqlite/ dbfile = abcs['test']['database'] || abcs['test']['dbfile'] - `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` + `sqlite3 #{dbfile} < "#{Rails.root}/db/#{Rails.env}_structure.sql"` when 'sqlserver' `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql` when 'oci', 'oracle' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2324039cf..5285060288 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -211,22 +211,20 @@ module ActiveRecord @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end - def association_primary_key - @association_primary_key ||= - options[:primary_key] || - !options[:polymorphic] && klass.primary_key || - 'id' + # klass option is necessary to support loading polymorphic associations + def association_primary_key(klass = nil) + options[:primary_key] || primary_key(klass || self.klass) end def active_record_primary_key - @active_record_primary_key ||= options[:primary_key] || active_record.primary_key + @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end def counter_cache_column 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 @@ -359,6 +357,10 @@ module ActiveRecord active_record.name.foreign_key end end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end end # Holds all the meta-data about a :through association as it was specified @@ -463,17 +465,15 @@ module ActiveRecord # We want to use the klass from this reflection, rather than just delegate straight to # the source_reflection, because the source_reflection may be polymorphic. We still # need to respect the source_reflection's :primary_key option, though. - def association_primary_key - @association_primary_key ||= begin - # Get the "actual" source reflection if the immediate source reflection has a - # source reflection itself - source_reflection = self.source_reflection - while source_reflection.source_reflection - source_reflection = source_reflection.source_reflection - end - - source_reflection.options[:primary_key] || klass.primary_key + def association_primary_key(klass = nil) + # Get the "actual" source reflection if the immediate source reflection has a + # source reflection itself + source_reflection = self.source_reflection + while source_reflection.source_reflection + source_reflection = source_reflection.source_reflection end + + source_reflection.options[:primary_key] || primary_key(klass || self.klass) end # Gets an array of possible <tt>:through</tt> source reflection names: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d0861d5c9..ecefaa633c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -68,7 +68,7 @@ module ActiveRecord end conn.insert( - im.to_sql, + im, 'SQL', primary_key, primary_key_value, @@ -94,6 +94,48 @@ module ActiveRecord scoping { @klass.create!(*args, &block) } end + # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. + # + # Expects arguments in the same format as <tt>Base.create</tt>. + # + # ==== Examples + # # Find the first user named Penélope or create a new one. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Penélope or create a new one. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Scarlett or create a new one with a particular last name. + # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson') + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # + # # Find the first user named Scarlett or create a new one with a different last name. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Scarlett').first_or_create do |user| + # user.last_name = "O'Hara" + # end + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + def first_or_create(attributes = nil, options = {}, &block) + first || create(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. + def first_or_create!(attributes = nil, options = {}, &block) + first || create!(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. + # + # Expects arguments in the same format as <tt>Base.new</tt>. + def first_or_initialize(attributes = nil, options = {}, &block) + first || new(attributes, options, &block) + end + def respond_to?(method, include_private = false) arel.respond_to?(method, include_private) || Array.method_defined?(method) || @@ -108,10 +150,10 @@ module ActiveRecord if default_scoped.equal?(self) @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) else IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) end end @@ -216,19 +258,21 @@ module ActiveRecord if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else - limit = nil - order = [] - # Apply limit and order only if they're both present - if @limit_value.present? == @order_values.present? - limit = arel.limit - order = arel.orders - end + stmt = Arel::UpdateManager.new(arel.engine) - stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) - stmt.take limit if limit - stmt.order(*order) + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) stmt.key = table[primary_key] - @klass.connection.update stmt.to_sql, 'SQL', bind_values + + 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 + + @klass.connection.update stmt, 'SQL', bind_values end end @@ -345,8 +389,7 @@ module ActiveRecord where(conditions).delete_all else statement = arel.compile_delete - affected = @klass.connection.delete( - statement.to_sql, 'SQL', bind_values) + affected = @klass.connection.delete(statement, 'SQL', bind_values) reset affected @@ -392,7 +435,7 @@ module ActiveRecord end def to_sql - @to_sql ||= arel.to_sql + @to_sql ||= klass.connection.to_sql(arel) end def where_values_hash 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/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0ac821b2d7..af86771d2d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -223,7 +223,7 @@ module ActiveRecord query_builder = relation.arel end - type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation) + type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -250,6 +250,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] + select_values += @select_values unless @having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| "#{field} AS #{aliaz}" @@ -258,7 +259,7 @@ module ActiveRecord relation = except(:group).group(group.join(',')) relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation.to_sql) + calculated_data = @klass.connection.select_all(relation) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8cef4e5554..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 @@ -180,7 +184,9 @@ module ActiveRecord # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? - def exists?(id = nil) + def exists?(id = false) + return false if id.nil? + id = id.id if ActiveRecord::Base === id join_dependency = construct_join_dependency_for_association_find @@ -193,8 +199,8 @@ module ActiveRecord else relation = relation.where(table[primary_key].eq(id)) if id end - - connection.select_value(relation.to_sql, "#{name} Exists") ? true : false + + connection.select_value(relation, "#{name} Exists") ? true : false end protected @@ -202,7 +208,7 @@ module ActiveRecord def find_with_associations join_dependency = construct_join_dependency_for_association_find relation = construct_relation_for_association_find(join_dependency) - rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values) + rows = connection.select_all(relation, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] 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 8bd4732c0c..670ba0987d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -37,6 +37,35 @@ module ActiveRecord relation end + # Works in two unique ways. + # + # First: takes a block so it can be used just like Array#select. + # + # Model.scoped.select { |m| m.field == value } + # + # This will build an array of objects from the database for the scope, + # converting them into an array and iterating through them using Array#select. + # + # Second: Modifies the SELECT statement for the query so that only certain + # fields are retrieved: + # + # >> Model.select(:field) + # => [#<Model field:value>] + # + # Although in the above example it looks as though this method returns an + # array, it actually returns a relation object and can have other query + # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. + # + # This method will also take multiple parameters: + # + # >> Model.select(:field, :other_field, :and_one_more) + # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # + # Any attributes that do not have fields retrieved by a select + # will return `nil` when the getter method for that attribute is used: + # + # >> Model.select(:field).first.other_field + # => nil def select(value = Proc.new) if block_given? to_a.select {|*block_args| value.call(*block_args) } @@ -147,6 +176,42 @@ module ActiveRecord relation end + # Used to extend a scope with additional methods, either through + # a module or through a block provided. + # + # The object returned is a relation, which can be further extended. + # + # === Using a module + # + # module Pagination + # def page(number) + # # pagination code goes here + # end + # end + # + # scope = Model.scoped.extending(Pagination) + # scope.page(params[:page]) + # + # You can also pass a list of modules: + # + # scope = Model.scoped.extending(Pagination, SomethingElse) + # + # === Using a block + # + # scope = Model.scoped.extending do + # def page(number) + # # pagination code goes here + # end + # end + # scope.page(params[:page]) + # + # You can also use a block and a module list: + # + # scope = Model.scoped.extending(Pagination) do + # def per_page(number) + # # pagination code goes here + # end + # end def extending(*modules) modules << Module.new(&Proc.new) if block_given? @@ -254,12 +319,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 +332,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 +341,6 @@ module ActiveRecord association.join_to(manager) end - manager.join_sources.concat join_nodes.uniq manager.join_sources.concat join_list manager @@ -311,6 +371,7 @@ module ActiveRecord o.reverse when String, Symbol o.to_s.split(',').collect do |s| + s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end else diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 19585f6214..6fe305f843 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -40,6 +40,10 @@ module ActiveRecord def header(stream) define_params = @version ? ":version => #{@version}" : "" + if stream.respond_to?(:external_encoding) + stream.puts "# encoding: #{stream.external_encoding.name}" + end + stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index cbfa1ad609..0e7f57aa43 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -179,7 +179,7 @@ module ActiveRecord #:nodoc: class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: def initialize(*args) super - options[:except] |= Array.wrap(@serializable.class.inheritance_column) + options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column) end class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb new file mode 100644 index 0000000000..d5910df891 --- /dev/null +++ b/activerecord/lib/active_record/store.rb @@ -0,0 +1,49 @@ +module ActiveRecord + # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. + # It's like a simple key/value store backed into your record when you don't care about being able to + # query that store outside the context of a single record. + # + # You can then declare accessors to this store that are then accessible just like any other attribute + # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's + # already built around just accessing attributes on the model. + # + # Make sure that you declare the database column used for the serialized store as a text, so there's + # plenty of room. + # + # Examples: + # + # class User < ActiveRecord::Base + # store :settings, accessors: [ :color, :homepage ] + # end + # + # u = User.new(color: 'black', homepage: '37signals.com') + # u.color # Accessor stored attribute + # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + # + # # Add additional accessors to an existing store through store_accessor + # class SuperUser < User + # store_accessor :settings, :privileges, :servants + # end + module Store + extend ActiveSupport::Concern + + module ClassMethods + def store(store_attribute, options = {}) + serialize store_attribute, Hash + store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + end + + def store_accessor(store_attribute, *keys) + Array(keys).flatten.each do |key| + define_method("#{key}=") do |value| + send(store_attribute)[key] = value + end + + define_method(key) do + send(store_attribute)[key] + end + end + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 1511c71ffc..4d5e469a7f 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: @@ -44,7 +48,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? + if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? + write_attribute(column.to_s, current_time) + end end end @@ -95,6 +101,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/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 5df85304a2..7af0352a31 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -17,15 +17,7 @@ module ActiveRecord # validates_associated :pages, :library # end # - # Warning: If, after the above definition, you then wrote: - # - # class Page < ActiveRecord::Base - # belongs_to :book - # - # validates_associated :book - # end - # - # this would specify a circular dependency and cause infinite recursion. + # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion. # # NOTE: This validation will not fail if the association hasn't been assigned. If you want to # ensure that the association is both present and guaranteed to be valid, you also need to diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 484b1d369b..2e2ea8c42b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -57,8 +57,8 @@ module ActiveRecord value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? if !options[:case_sensitive] && value && column.text? - # will use SQL LOWER function before comparison - relation = table[attribute].lower.eq(table.lower(value)) + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) else value = klass.connection.case_sensitive_modifier(value) relation = table[attribute].eq(value) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 3942e7bb41..94497e37c7 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -151,7 +151,20 @@ class AdapterTest < ActiveRecord::TestCase else @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" end + # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block + # and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" end end end + + def test_deprecated_visitor_for + visitor_klass = Class.new(Arel::Visitors::ToSql) + Arel::Visitors::VISITORS['fuuu'] = visitor_klass + pool = stub(:spec => stub(:config => { :adapter => 'fuuu' })) + visitor = assert_deprecated { + ActiveRecord::ConnectionAdapters::AbstractAdapter.visitor_for(pool) + } + assert visitor.is_a?(visitor_klass) + 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/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb new file mode 100644 index 0000000000..5ffd886dab --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -0,0 +1,35 @@ +require "cases/helper" +require 'models/person' + +class MysqlCaseSensitivityTest < ActiveRecord::TestCase + class CollationTest < ActiveRecord::Base + validates_uniqueness_of :string_cs_column, :case_sensitive => false + validates_uniqueness_of :string_ci_column, :case_sensitive => false + end + + def test_columns_include_collation_different_from_table + assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation + assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation + end + + def test_case_sensitive + assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? + assert CollationTest.columns_hash['string_cs_column'].case_sensitive? + end + + def test_case_insensitive_comparison_for_ci_column + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'a') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match /string_ci_column/ } + assert_no_match(/lower/i, ci_uniqueness_query) + end + + def test_case_insensitive_comparison_for_cs_column + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'a') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match /string_cs_column/ } + assert_match(/lower/i, cs_uniqueness_query) + end +end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb new file mode 100644 index 0000000000..83de90f179 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -0,0 +1,23 @@ +require 'cases/helper' + +module ActiveRecord::ConnectionAdapters + class MysqlAdapter + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb new file mode 100644 index 0000000000..6bcc113482 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -0,0 +1,35 @@ +require "cases/helper" +require 'models/person' + +class Mysql2CaseSensitivityTest < ActiveRecord::TestCase + class CollationTest < ActiveRecord::Base + validates_uniqueness_of :string_cs_column, :case_sensitive => false + validates_uniqueness_of :string_ci_column, :case_sensitive => false + end + + def test_columns_include_collation_different_from_table + assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation + assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation + end + + def test_case_sensitive + assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? + assert CollationTest.columns_hash['string_cs_column'].case_sensitive? + end + + def test_case_insensitive_comparison_for_ci_column + CollationTest.create!(:string_ci_column => 'A') + invalid = CollationTest.new(:string_ci_column => 'a') + queries = assert_sql { invalid.save } + ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } + assert_no_match(/lower/i, ci_uniqueness_query) + end + + def test_case_insensitive_comparison_for_cs_column + CollationTest.create!(:string_cs_column => 'A') + invalid = CollationTest.new(:string_cs_column => 'a') + queries = assert_sql { invalid.save } + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)} + assert_match(/lower/i, cs_uniqueness_query) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 3a7f1badf0..b01eabc840 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -38,6 +38,10 @@ class SchemaTest < ActiveRecord::TestCase set_table_name 'test_schema."Things"' end + class Thing5 < ActiveRecord::Base + set_table_name 'things' + end + def setup @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @@ -91,6 +95,12 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_table_exists_quoted_table + with_schema_search_path(SCHEMA_NAME) do + assert(@connection.table_exists?('"things.table"'), "table should exist") + end + end + def test_with_schema_prefixed_table_name assert_nothing_raised do assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") @@ -219,21 +229,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', @@ -245,6 +240,21 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_prepared_statements_with_multiple_schemas + + @connection.schema_search_path = SCHEMA_NAME + Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now) + + @connection.schema_search_path = SCHEMA2_NAME + Thing5.create(:id => 1, :name => "thing inside #{SCHEMA2_NAME}", :email => "thing1@localhost", :moment => Time.now) + + @connection.schema_search_path = SCHEMA_NAME + assert_equal 1, Thing5.count + + @connection.schema_search_path = SCHEMA2_NAME + assert_equal 1, Thing5.count + end + def test_schema_exists? { 'public' => true, diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb new file mode 100644 index 0000000000..a82c6f67d6 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -0,0 +1,23 @@ +require 'cases/helper' + +module ActiveRecord::ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end + end +end 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..eb6f071dc1 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,9 +1,12 @@ # encoding: utf-8 require "cases/helper" +require 'models/owner' module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + class DualEncoding < ActiveRecord::Base end @@ -19,6 +22,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]] @@ -139,6 +157,8 @@ module ActiveRecord binary = DualEncoding.new :name => 'いただきます!', :data => str binary.save! assert_equal str, binary.data + + DualEncoding.connection.drop_table('dual_encodings') end def test_execute diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb new file mode 100644 index 0000000000..ae272e2c4b --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -0,0 +1,24 @@ +require 'cases/helper' + +module ActiveRecord::ConnectionAdapters + class SQLiteAdapter + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end + end +end + diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 818902beb5..1160d236c9 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -13,6 +13,7 @@ require 'models/comment' require 'models/sponsor' require 'models/member' require 'models/essay' +require 'models/toy' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -352,6 +353,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" # ' @@ -690,4 +697,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal nil, comment.reload.parent assert_equal 0, comments(:greetings).reload.children_count end + + def test_polymorphic_with_custom_primary_key + toy = Toy.create! + sponsor = Sponsor.create!(:sponsorable => toy) + + assert_equal toy, sponsor.reload.sponsorable + end end 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_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index d8d2a113ff..34d90cc395 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -650,6 +650,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker end + def test_habtm_selects_all_columns_by_default + assert_equal Project.column_names.sort, developers(:david).projects.first.attributes.keys.sort + end + + def test_habtm_respects_select_query_method + assert_equal ['id'], developers(:david).projects.select(:id).first.attributes.keys + end + def test_join_table_alias assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size 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..cddd2a6f8c 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 @@ -484,6 +485,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, authors(:mary).popular_grouped_posts.length end + def test_default_select + assert_equal Comment.column_names.sort, posts(:welcome).comments.first.attributes.keys.sort + end + + def test_select_query_method + assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys + end + def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") @@ -850,6 +859,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 @@ -1568,4 +1586,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.attributes_after_initialize['car_id'] end + + def test_replace + car = Car.create(:name => 'honda') + bulb1 = car.bulbs.create + bulb2 = Bulb.create + + assert_equal [bulb1], car.bulbs + car.bulbs.replace([bulb2]) + assert_equal [bulb2], car.bulbs + assert_equal [bulb2], car.reload.bulbs + end end 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 dd450a2a8e..530f5212a2 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -247,7 +247,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins assert_includes_and_joins_equal( - Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'), + Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'), [categories(:general), categories(:technology)], :post_comments ) end @@ -356,6 +356,17 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal categories(:general), members(:groucho).club_category end + def test_joins_and_includes_from_through_models_not_included_in_association + prev_default_scope = Club.default_scopes + + [:includes, :preload, :joins, :eager_load].each do |q| + Club.default_scopes = [Club.send(q, :category)] + assert_equal categories(:general), members(:groucho).reload.club_category + end + 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/associations_test.rb b/activerecord/test/cases/associations_test.rb index 49d82ba2df..ffe2993e0f 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -203,6 +203,11 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.reload.reload end end + + def test_proxy_association_accessor + david = developers(:david) + assert_equal david.association(:projects), david.projects.proxy_association + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 3641031d12..e03ed33591 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -35,6 +35,7 @@ module ActiveRecord end def self.serialized_attributes; {}; end + def self.base_class; self; end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index b0896fb236..b1b41fed0d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -113,6 +113,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase # by inspecting it. def test_allocated_object_can_be_inspected topic = Topic.allocate + topic.instance_eval { @attributes = nil } assert_nothing_raised { topic.inspect } assert topic.inspect, "#<Topic not initialized>" end @@ -430,30 +431,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert topic.is_test? end - def test_kernel_methods_not_implemented_in_activerecord - %w(test name display y).each do |method| - assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined" - end - end - - def test_defined_kernel_methods_implemented_in_model - %w(test name display y).each do |method| - klass = Class.new ActiveRecord::Base - klass.class_eval "def #{method}() 'defined #{method}' end" - assert klass.instance_method_already_implemented?(method), "##{method} is not defined" - end - end - - def test_defined_kernel_methods_implemented_in_model_abstract_subclass - %w(test name display y).each do |method| - abstract = Class.new ActiveRecord::Base - abstract.class_eval "def #{method}() 'defined #{method}' end" - abstract.abstract_class = true - klass = Class.new abstract - assert klass.instance_method_already_implemented?(method), "##{method} is not defined" - end - end - def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model %w(save create_or_update).each do |method| klass = Class.new ActiveRecord::Base @@ -607,7 +584,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(:title => "The pros and cons of programming naked.") assert !topic.respond_to?(:title) exception = assert_raise(NoMethodError) { topic.title } - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") assert_equal "I'm private", topic.send(:title) end @@ -617,7 +594,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new assert !topic.respond_to?(:title=) exception = assert_raise(NoMethodError) { topic.title = "Pants"} - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") topic.send(:title=, "Very large pants") end @@ -627,7 +604,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(:title => "Isaac Newton's pants") assert !topic.respond_to?(:title?) exception = assert_raise(NoMethodError) { topic.title? } - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") assert topic.send(:title?) end @@ -658,6 +635,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal %w(preferences), Contact.serialized_attributes.keys end + def test_instance_method_should_be_defined_on_the_base_class + subklass = Class.new(Topic) + + Topic.define_attribute_methods + + instance = subklass.new + instance.id = 5 + assert_equal 5, instance.id + assert subklass.method_defined?(:id), "subklass is missing id method" + + Topic.undefine_attribute_methods + + assert_equal 5, instance.id + assert subklass.method_defined?(:id), "subklass is missing id method" + end + + def test_dispatching_column_attributes_through_method_missing_deprecated + Topic.define_attribute_methods + + topic = Topic.new(:id => 5) + topic.id = 5 + + topic.method(:id).owner.send(:remove_method, :id) + + assert_deprecated do + assert_equal 5, topic.id + end + ensure + Topic.undefine_attribute_methods + end + private def cached_columns @cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8b4e7dd799..12c1cfb30e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -21,8 +21,11 @@ require 'models/parrot' require 'models/person' require 'models/edge' require 'models/joke' +require 'models/bulb' +require 'models/bird' require 'rexml/document' require 'active_support/core_ext/exception' +require 'bcrypt' class Category < ActiveRecord::Base; end class Categorization < ActiveRecord::Base; end @@ -66,6 +69,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' @@ -143,25 +169,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_use_table_engine_for_quoting_where - relation = Topic.where(Topic.arel_table[:id].eq(1)) - engine = relation.table.engine - - fakepool = Class.new(Struct.new(:spec)) { - def with_connection; yield self; end - def connection_pool; self; end - def table_exists?(name); false; end - def quote_table_name(*args); raise "lol quote_table_name"; end - } - - relation.table.engine = fakepool.new(engine.connection_pool.spec) - - error = assert_raises(RuntimeError) { relation.to_a } - assert_match('lol', error.message) - ensure - relation.table.engine = engine - end - def test_preserving_time_objects assert_kind_of( Time, Topic.find(1).bonus_time, @@ -260,6 +267,41 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_create_after_initialize_without_block + cb = CustomBulb.create(:name => 'Dude') + assert_equal('Dude', cb.name) + assert_equal(true, cb.frickinawesome) + end + + def test_create_after_initialize_with_block + cb = CustomBulb.create {|c| c.name = 'Dude' } + assert_equal('Dude', cb.name) + assert_equal(true, cb.frickinawesome) + end + + def test_first_or_create + parrot = Bird.first_or_create(:color => 'green', :name => 'parrot') + assert parrot.persisted? + the_same_parrot = Bird.first_or_create(:color => 'yellow', :name => 'macaw') + assert_equal parrot, the_same_parrot + end + + def test_first_or_create_bang + assert_raises(ActiveRecord::RecordInvalid) { Bird.first_or_create! } + parrot = Bird.first_or_create!(:color => 'green', :name => 'parrot') + assert parrot.persisted? + the_same_parrot = Bird.first_or_create!(:color => 'yellow', :name => 'macaw') + assert_equal parrot, the_same_parrot + end + + def test_first_or_initialize + parrot = Bird.first_or_initialize(:color => 'green', :name => 'parrot') + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert parrot.new_record? + assert parrot.valid? + end + def test_load topics = Topic.find(:all, :order => 'id') assert_equal(4, topics.size) @@ -1614,6 +1656,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 @@ -1813,12 +1859,45 @@ 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 + 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 + + marshalled = Marshal.dump(post) + post = Marshal.load(marshalled) + + assert_equal 1, post.comments.length + end + def test_attribute_names assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], Company.attribute_names @@ -1854,6 +1933,6 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first dev.update_attribute(:updated_at, nil) - assert_match /\/#{dev.id}$/, dev.cache_key + assert_match(/\/#{dev.id}$/, dev.cache_key) end 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/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 224b3f3d1f..c38814713a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -170,6 +170,13 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, c[2] end + def test_should_group_by_summed_field_having_condition_from_select + c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit) + assert_nil c[1] + assert_equal 60, c[2] + assert_equal 53, c[9] + end + def test_should_group_by_summed_association c = Account.sum(:credit_limit, :group => :firm) assert_equal 50, c[companies(:first_firm)] 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/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index f92f4e62c5..8a0f453127 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -135,6 +135,10 @@ module ActiveRecord pool.with_connection end end + + def test_pool_sets_connection_visitor + assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql) + end end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5dc5f99582..3088ab012f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -48,6 +48,15 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists? end + # exists? should handle nil for id's that come from URLs and always return false + # (example: Topic.exists?(params[:id])) where params[:id] is nil + def test_exists_with_nil_arg + assert !Topic.exists?(nil) + assert Topic.exists? + assert !Topic.first.replies.exists?(nil) + assert Topic.first.replies.exists? + end + def test_does_not_exist_with_empty_table_and_no_args_given Topic.delete_all assert !Topic.exists? @@ -243,6 +252,32 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } + assert_sql(/LIMIT 5|ROWNUM <= 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 842e8a0049..7e2dafcd01 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: : ' + 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' @@ -435,14 +451,36 @@ end class CustomConnectionFixturesTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - # Set to false to blow away fixtures cache and ensure our fixtures are loaded - # and thus takes into account our set_fixture_class self.use_transactional_fixtures = false def test_connection assert_kind_of Course, courses(:ruby) assert_equal Course.connection, courses(:ruby).connection end + + def test_leaky_destroy + assert_nothing_raised { courses(:ruby) } + courses(:ruby).destroy + end + + def test_it_twice_in_whatever_order_to_check_for_fixture_leakage + test_leaky_destroy + end +end + +class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase + set_fixture_class :courses => Course + fixtures :courses + self.use_transactional_fixtures = true + + def test_leaky_destroy + assert_nothing_raised { courses(:ruby) } + courses(:ruby).destroy + end + + def test_it_twice_in_whatever_order_to_check_for_fixture_leakage + test_leaky_destroy + end end class InvalidTableNameFixturesTest < ActiveRecord::TestCase @@ -480,7 +518,9 @@ class ManyToManyFixturesWithClassDefined < ActiveRecord::TestCase end class FixturesBrokenRollbackTest < ActiveRecord::TestCase - def blank_setup; end + def blank_setup + @fixture_connections = [ActiveRecord::Base.connection] + end alias_method :ar_setup_fixtures, :setup_fixtures alias_method :setup_fixtures, :blank_setup alias_method :setup, :blank_setup @@ -587,8 +627,8 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_preserves_existing_fixture_data - assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).created_on.utc.to_date) - assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).updated_on.utc.to_date) + assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date) + assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date) end def test_generates_unique_ids diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index cd9c358799..3efc8bf559 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -164,7 +164,7 @@ class IdentityMapTest < ActiveRecord::TestCase end ############################################################################## - # Tests checking dirty attribute behaviour with IM # + # Tests checking dirty attribute behavior with IM # ############################################################################## def test_loading_new_instance_should_not_update_dirty_attributes @@ -238,7 +238,7 @@ class IdentityMapTest < ActiveRecord::TestCase end ############################################################################## - # Tests checking Identity Map behaviour with preloaded associations, joins, # + # Tests checking Identity Map behavior with preloaded associations, joins, # # includes etc. # ############################################################################## diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 2de50b224c..98cda010ae 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -24,9 +24,9 @@ class InvalidDateTest < ActiveRecord::TestCase topic = Topic.new({"last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s}) # Oracle DATE columns are datetime columns and Oracle adapter returns Time value if current_adapter?(:OracleAdapter) - assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") else - assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") end end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index afec64750e..3ae7b63dff 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -27,6 +27,19 @@ module ActiveRecord end end + class LegacyMigration < ActiveRecord::Migration + def self.up + create_table("horses") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + + def self.down + drop_table("horses") + end + end + def teardown if ActiveRecord::Base.connection.table_exists?("horses") ActiveRecord::Base.connection.drop_table("horses") @@ -41,17 +54,39 @@ module ActiveRecord end end - def test_up + def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) assert migration.connection.table_exists?("horses"), "horses should exist" end - def test_down + def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down assert !migration.connection.table_exists?("horses") end + + def test_legacy_up + LegacyMigration.migrate :up + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + end + + def test_legacy_down + LegacyMigration.migrate :up + LegacyMigration.migrate :down + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + end + + def test_up + LegacyMigration.up + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + end + + def test_down + LegacyMigration.up + LegacyMigration.down + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + end end end 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/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index a0cb5dbdc5..0ab4f30363 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -14,7 +14,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_set_conditions Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do - assert_match '(just a test...)', Developer.scoped.arel.to_sql + assert_match '(just a test...)', Developer.scoped.to_sql end end @@ -274,7 +274,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do Developer.send(:with_scope, :find => { :limit => 10 }) do devs = Developer.scoped - assert_match '(salary = 80000)', devs.arel.to_sql + assert_match '(salary = 80000)', devs.to_sql assert_equal 10, devs.taken end end @@ -308,7 +308,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do devs = Developer.scoped - assert_match "(name = 'David') AND (salary = 80000)", devs.arel.to_sql + assert_match "(name = 'David') AND (salary = 80000)", devs.to_sql assert_equal(1, Developer.count) end Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do @@ -321,7 +321,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do devs = Developer.scoped - assert_match "(salary = 80000) AND (name = 'David')", devs.arel.to_sql + assert_match "(salary = 80000) AND (name = 'David')", devs.to_sql assert_equal 10, devs.taken end end 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/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 57d1441128..adfd8e83a1 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -29,6 +29,26 @@ class PersistencesTest < ActiveRecord::TestCase end end + def test_update_all_doesnt_ignore_order + assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error + test_update_with_order_succeeds = lambda do |order| + begin + Author.order(order).update_all('id = id + 1') + rescue ActiveRecord::ActiveRecordError + false + end + end + + if test_update_with_order_succeeds.call('id DESC') + assert !test_update_with_order_succeeds.call('id ASC') # test that this wasn't a fluke and using an incorrect order results in an exception + else + # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do + test_update_with_order_succeeds.call('id DESC') + end + end + end + def test_update_all_with_order_and_limit_updates_subset_only author = authors(:david) assert_nothing_raised do @@ -182,9 +202,12 @@ class PersistencesTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.new - topic.title = 'Another New Topic' - topic.send :write_attribute, 'does_not_exist', 'test' + topic = Topic.allocate.init_with( + 'attributes' => { + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' + } + ) assert_nothing_raised { topic.save } end @@ -229,9 +252,11 @@ class PersistencesTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topicReloaded = Topic.find(topic.id) - topicReloaded.title = "A New Topic" - topicReloaded.send :write_attribute, 'does_not_exist', 'test' + topicReloaded = Topic.allocate + topicReloaded.init_with( + 'attributes' => topic.attributes.merge('does_not_exist' => 'test') + ) + topicReloaded.title = 'A New Topic' assert_nothing_raised { topicReloaded.save } end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 379cf5b44e..434b8a677a 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -3,6 +3,8 @@ require "models/project" require "timeout" class PooledConnectionsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + def setup @per_test_teardown = [] @connection = ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 7e3da145e5..4bb5752096 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -26,7 +26,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_to_key_with_primary_key_after_destroy topic = Topic.find(1) topic.destroy - assert_equal nil, topic.to_key + assert_equal [1], topic.to_key end def test_integer_key @@ -146,3 +146,23 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key end end + +class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_set_primary_key_with_no_connection + return skip("disconnect wipes in-memory db") if in_memory_db? + + connection = ActiveRecord::Base.remove_connection + + model = Class.new(ActiveRecord::Base) do + set_primary_key 'foo' + end + + assert_equal 'foo', model.primary_key + + ActiveRecord::Base.establish_connection(connection) + + assert_equal 'foo', model.primary_key + end +end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index ad17f6f83a..b2429d631f 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| @@ -121,13 +147,16 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_does_not_wrap_string_results_in_arrays - require 'sqlite3/version' if current_adapter?(:SQLite3Adapter) + if current_adapter?(:SQLite3Adapter) + require 'sqlite3/version' + sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION + end Task.cache do # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter) && SQLite3::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -208,9 +237,9 @@ class QueryCacheBodyProxyTest < ActiveRecord::TestCase test "is polite to it's body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new - proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body) + proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body, ActiveRecord::Base.connection_id) assert proxy.respond_to?(:to_path) assert_equal proxy.to_path, "/path" end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 41312e8661..69e9fc8d61 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -18,6 +18,7 @@ require 'models/subscriber' require 'models/subscription' require 'models/tag' require 'models/sponsor' +require 'models/edge' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -244,7 +245,7 @@ class ReflectionTest < ActiveRecord::TestCase # Normal association assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s - assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s + assert_equal "name", Essay.reflect_on_association(:writer).association_primary_key.to_s # Through association (uses the :primary_key option from the source reflection) assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s @@ -252,11 +253,25 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end + def test_association_primary_key_raises_when_missing_primary_key + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, {}, Author) + assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } + + through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, {}, Author) + through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge')) + assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } + end + def test_active_record_primary_key assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s end + def test_active_record_primary_key_raises_when_missing_primary_key + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, {}, Edge) + assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } + end + def test_foreign_type assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index f2d177d834..1e2093273e 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -170,7 +170,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase Developer.where('salary = 80000').scoping do Developer.limit(10).scoping do devs = Developer.scoped - assert_match '(salary = 80000)', devs.arel.to_sql + assert_match '(salary = 80000)', devs.to_sql assert_equal 10, devs.taken end end @@ -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 0aaa0342b1..95408a5f29 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) { @@ -840,6 +863,128 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'hen', hen.name end + def test_first_or_create + parrot = Bird.where(:color => 'green').first_or_create(:name => 'parrot') + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + + same_parrot = Bird.where(:color => 'green').first_or_create(:name => 'parakeet') + assert_kind_of Bird, same_parrot + assert same_parrot.persisted? + assert_equal parrot, same_parrot + end + + def test_first_or_create_with_no_parameters + parrot = Bird.where(:color => 'green').first_or_create + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert_equal 'green', parrot.color + end + + def test_first_or_create_with_block + parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + + same_parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parakeet' } + assert_equal parrot, same_parrot + end + + def test_first_or_create_with_array + several_green_birds = Bird.where(:color => 'green').first_or_create([{:name => 'parrot'}, {:name => 'parakeet'}]) + assert_kind_of Array, several_green_birds + several_green_birds.each { |bird| assert bird.persisted? } + + same_parrot = Bird.where(:color => 'green').first_or_create([{:name => 'hummingbird'}, {:name => 'macaw'}]) + assert_kind_of Bird, same_parrot + assert_equal several_green_birds.first, same_parrot + end + + def test_first_or_create_bang_with_valid_options + parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parrot') + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + + same_parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parakeet') + assert_kind_of Bird, same_parrot + assert same_parrot.persisted? + assert_equal parrot, same_parrot + end + + def test_first_or_create_bang_with_invalid_options + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!(:pirate_id => 1) } + end + + def test_first_or_create_bang_with_no_parameters + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create! } + end + + def test_first_or_create_bang_with_valid_block + parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + + same_parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parakeet' } + assert_equal parrot, same_parrot + end + + def test_first_or_create_bang_with_invalid_block + assert_raise(ActiveRecord::RecordInvalid) do + Bird.where(:color => 'green').first_or_create! { |bird| bird.pirate_id = 1 } + end + end + + def test_first_or_create_with_valid_array + several_green_birds = Bird.where(:color => 'green').first_or_create!([{:name => 'parrot'}, {:name => 'parakeet'}]) + assert_kind_of Array, several_green_birds + several_green_birds.each { |bird| assert bird.persisted? } + + same_parrot = Bird.where(:color => 'green').first_or_create!([{:name => 'hummingbird'}, {:name => 'macaw'}]) + assert_kind_of Bird, same_parrot + assert_equal several_green_birds.first, same_parrot + end + + def test_first_or_create_with_invalid_array + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!([ {:name => 'parrot'}, {:pirate_id => 1} ]) } + end + + def test_first_or_initialize + parrot = Bird.where(:color => 'green').first_or_initialize(:name => 'parrot') + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert parrot.valid? + assert parrot.new_record? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + end + + def test_first_or_initialize_with_no_parameters + parrot = Bird.where(:color => 'green').first_or_initialize + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert !parrot.valid? + assert parrot.new_record? + assert_equal 'green', parrot.color + end + + def test_first_or_initialize_with_block + parrot = Bird.where(:color => 'green').first_or_initialize { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert parrot.valid? + assert parrot.new_record? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + end + def test_explicit_create_scope hens = Bird.where(:name => 'hen') assert_equal 'hen', hens.new.name @@ -961,4 +1106,46 @@ class RelationTest < ActiveRecord::TestCase assert scope.eager_loading? end + + 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/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 4adecf8e83..71ff727b7f 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,13 +1,22 @@ require "cases/helper" -require 'stringio' class SchemaDumperTest < ActiveRecord::TestCase + def setup + @stream = StringIO.new + end + def standard_dump - stream = StringIO.new + @stream = StringIO.new ActiveRecord::SchemaDumper.ignore_tables = [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - stream.string + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream) + @stream.string + end + + if "string".encoding_aware? + def test_magic_comment + assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + end end def test_schema_dump diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 382d659289..61b04b3e37 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -7,12 +7,13 @@ class SerializationTest < ActiveRecord::TestCase def setup @contact_attributes = { - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => '<strong>ruby</strong>' } + :name => 'aaron stack', + :age => 25, + :avatar => 'binarydata', + :created_at => Time.utc(2006, 8, 1), + :awesome => false, + :preferences => { :gem => '<strong>ruby</strong>' }, + :alternative_id => nil } end diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index 669c0b7b4d..258cee7aba 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -36,6 +36,7 @@ module ActiveRecord end def test_find_by_sess_id_compat + Session.reset_column_information klass = Class.new(Session) do def self.session_id_column 'sessid' @@ -53,6 +54,7 @@ module ActiveRecord assert_equal session.sessid, found.session_id ensure klass.drop_table! + Session.reset_column_information end def test_find_by_session_id diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb new file mode 100644 index 0000000000..3d056d93b6 --- /dev/null +++ b/activerecord/test/cases/store_test.rb @@ -0,0 +1,29 @@ +require 'cases/helper' +require 'models/admin' +require 'models/admin/user' + +class StoreTest < ActiveRecord::TestCase + setup do + @john = Admin::User.create(name: 'John Doe', color: 'black') + end + + test "reading store attributes through accessors" do + assert_equal 'black', @john.color + assert_nil @john.homepage + end + + test "writing store attributes through accessors" do + @john.color = 'red' + @john.homepage = '37signals.com' + + assert_equal 'red', @john.color + assert_equal '37signals.com', @john.homepage + end + + test "accessing attributes not exposed by accessors" do + @john.settings[:icecream] = 'graeters' + @john.save + + assert 'graeters', @john.reload.settings[:icecream] + end +end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index f85fb4e5da..e82ca3f93d 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestUnconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_fixtures = false def setup @underlying = ActiveRecord::Base.connection diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 8c1a45430e..f450efd839 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -37,11 +37,13 @@ connections: db2: arunit: + adapter: ibm_db host: localhost username: arunit password: arunit database: arunit arunit2: + adapter: ibm_db host: localhost username: arunit password: arunit diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml index abb91101da..6004f390a4 100644 --- a/activerecord/test/fixtures/pirates.yml +++ b/activerecord/test/fixtures/pirates.yml @@ -5,5 +5,5 @@ blackbeard: redbeard: catchphrase: "Avast!" parrot: louis - created_on: <%= 2.weeks.ago.to_s(:db) %> - updated_on: <%= 2.weeks.ago.to_s(:db) %> + created_on: "<%= 2.weeks.ago.to_s(:db) %>" + updated_on: "<%= 2.weeks.ago.to_s(:db) %>" 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/admin/user.rb b/activerecord/test/models/admin/user.rb index 74bb21551e..275a03c344 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,3 +1,4 @@ class Admin::User < ActiveRecord::Base belongs_to :account + store :settings, accessors: [ :color, :homepage ] end
\ No newline at end of file diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index efb98b66e7..888afc7604 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -33,4 +33,9 @@ class Bulb < ActiveRecord::Base end class CustomBulb < Bulb + after_initialize :set_awesomeness + + def set_awesomeness + self.frickinawesome = true if name == 'Dude' + end end 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/contact.rb b/activerecord/test/models/contact.rb index e081eee661..3d15c7fbed 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -11,12 +11,13 @@ class Contact < ActiveRecord::Base connection.merge_column('contacts', name, sql_type, options) end - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string + column :name, :string + column :age, :integer + column :avatar, :binary + column :created_at, :datetime + column :awesome, :boolean + column :preferences, :string + column :alternative_id, :integer serialize :preferences 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/topic.rb b/activerecord/test/models/topic.rb index 6440dbe8ab..fe424e61b2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -78,11 +78,12 @@ class Topic < ActiveRecord::Base after_initialize :set_email_address + def approved=(val) + @custom_approved = val + write_attribute(:approved, val) + end + protected - def approved=(val) - @custom_approved = val - write_attribute(:approved, val) - end def default_written_on self.written_on = Time.now unless attribute_present?("written_on") 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 diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index c78d99f4af..ab2c7ccc10 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -21,4 +21,15 @@ BEGIN END SQL -end + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS collation_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE collation_tests ( + string_cs_column VARCHAR(1) COLLATE utf8_bin, + string_ci_column VARCHAR(1) COLLATE utf8_general_ci +) CHARACTER SET utf8 COLLATE utf8_general_ci +SQL + +end
\ No newline at end of file diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index 30e1c5a167..a0adfe3752 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -32,4 +32,15 @@ BEGIN END SQL + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS collation_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE collation_tests ( + string_cs_column VARCHAR(1) COLLATE utf8_bin, + string_ci_column VARCHAR(1) COLLATE utf8_general_ci +) CHARACTER SET utf8 COLLATE utf8_general_ci +SQL + end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 64e0452100..bb08f5c181 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,6 +37,7 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name + t.text :settings t.references :account end @@ -47,6 +48,7 @@ ActiveRecord::Schema.define do create_table :audit_logs, :force => true do |t| t.column :message, :string, :null=>false t.column :developer_id, :integer, :null=>false + t.integer :unvalidated_developer_id end create_table :authors, :force => true do |t| @@ -156,6 +158,7 @@ ActiveRecord::Schema.define do t.string :type t.integer :taggings_count, :default => 0 t.integer :children_count, :default => 0 + t.integer :parent_id end create_table :companies, :force => true do |t| @@ -461,6 +464,7 @@ ActiveRecord::Schema.define do create_table :pirates, :force => true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer + t.integer :non_validated_parrot_id t.column :created_on, :datetime t.column :updated_on, :datetime end @@ -529,6 +533,7 @@ ActiveRecord::Schema.define do create_table :ships, :force => true do |t| t.string :name t.integer :pirate_id + t.integer :update_only_pirate_id t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -663,7 +668,9 @@ ActiveRecord::Schema.define do t.string :description t.integer :man_id t.integer :polymorphic_man_id - t.string :polymorphic_man_type + t.string :polymorphic_man_type + t.integer :horrible_polymorphic_man_id + t.string :horrible_polymorphic_man_type end create_table :interests, :force => true do |t| |