diff options
Diffstat (limited to 'activerecord')
72 files changed, 1658 insertions, 726 deletions
diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e536d2b408..93eb42a52c 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,57 @@ *Rails 3.1.0 (unreleased)* +* Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your + scope to be lazily evaluated, or takes parameters, please define it as a normal class method + instead. For example, change this: + + class Post < ActiveRecord::Base + scope :unpublished, lambda { where('published_at > ?', Time.now) } + end + + To this: + + class Post < ActiveRecord::Base + def self.unpublished + where('published_at > ?', Time.now) + end + end + + [Jon Leighton] + +* Default scopes are now evaluated at the latest possible moment, to avoid problems where + scopes would be created which would implicitly contain the default scope, which would then + be impossible to get rid of via Model.unscoped. + + Note that this means that if you are inspecting the internal structure of an + ActiveRecord::Relation, it will *not* contain the default scope, though the resulting + query will do. You can get a relation containing the default scope by calling + ActiveRecord#with_default_scope, though this is not part of the public API. + + [Jon Leighton] + +* Deprecated support for passing hashes and relations to 'default_scope'. Please create a class + method for your scope instead. For example, change this: + + class Post < ActiveRecord::Base + default_scope where(:published => true) + end + + To this: + + class Post < ActiveRecord::Base + def self.default_scope + where(:published => true) + end + end + + Rationale: It will make the implementation simpler because we can simply use inheritance to + handle inheritance scenarios, rather than trying to make up our own rules about what should + happen when you call default_scope multiple times or in subclasses. + + [Jon Leighton] + +* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. + * ConnectionManagement middleware is changed to clean up the connection pool after the rack body has been flushed. diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 27c446b12c..687b668634 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module Associations @@ -163,7 +164,7 @@ module ActiveRecord def creation_attributes attributes = {} - if [:has_one, :has_many].include?(reflection.macro) && !options[:through] + if reflection.macro.in?([:has_one, :has_many]) && !options[:through] attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key] if reflection.options[:as] diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 964e7fddc8..f6d26840c2 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class BelongsTo < SingularAssociation #:nodoc: self.macro = :belongs_to @@ -65,7 +67,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete].include?(options[:dependent]) + unless options[:dependent].in?([:destroy, :delete]) raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 77bb66228d..ecbc70888f 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class HasMany < CollectionAssociation #:nodoc: self.macro = :has_many @@ -14,7 +16,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent]) + unless options[:dependent].in?([:destroy, :delete_all, :nullify, :restrict]) raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \ ":nullify or :restrict (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 07ba5d088e..88c0d3e90f 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord::Associations::Builder class HasOne < SingularAssociation #:nodoc: self.macro = :has_one @@ -27,7 +29,7 @@ module ActiveRecord::Associations::Builder def configure_dependency if options[:dependent] - unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent]) + unless options[:dependent].in?([:destroy, :delete, :nullify, :restrict]) raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \ ":nullify or :restrict (#{options[:dependent].inspect})" end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 9f4fc44cc6..33a184d48d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -21,14 +21,7 @@ module ActiveRecord attr_reader :proxy def initialize(owner, reflection) - # When scopes are created via method_missing on the proxy, they are stored so that - # any records fetched from the database are kept around for future use. - @scopes_cache = Hash.new do |hash, method| - hash[method] = { } - end - super - @proxy = CollectionProxy.new(self) end @@ -74,7 +67,6 @@ module ActiveRecord def reset @loaded = false @target = [] - @scopes_cache.clear end def select(select = nil) @@ -327,10 +319,6 @@ module ActiveRecord end end - def cached_scope(method, args) - @scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args) - end - def load_target if find_target? targets = [] diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index cf77d770c9..388173c1fb 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -82,8 +82,6 @@ module ActiveRecord end end - elsif @association.klass.scopes[method] - @association.cached_scope(method, args) else scoped.readonly(nil).send(method, *args, &block) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 1d2e8667e4..7134dc85c8 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + module ActiveRecord # = Active Record Belongs To Has One Association module Associations @@ -50,7 +52,7 @@ module ActiveRecord end def remove_target!(method) - if [:delete, :destroy].include?(method) + if method.in?([:delete, :destroy]) target.send(method) else nullify_owner_attributes(target) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 4121a5b378..0a666598ed 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -89,14 +89,7 @@ module ActiveRecord foreign_key = reflection.active_record_primary_key end - constraint = table[key].eq(foreign_table[foreign_key]) - - if reflection.klass.finder_needs_type_condition? - constraint = table.create_and([ - constraint, - reflection.klass.send(:type_condition, table) - ]) - end + constraint = build_constraint(reflection, table, key, foreign_table, foreign_key) relation.from(join(table, constraint)) @@ -111,6 +104,19 @@ module ActiveRecord relation end + def build_constraint(reflection, table, key, foreign_table, foreign_key) + constraint = table[key].eq(foreign_table[foreign_key]) + + if reflection.klass.finder_needs_type_condition? + constraint = table.create_and([ + constraint, + reflection.klass.send(:type_condition, table) + ]) + end + + constraint + end + def join_relation(joining_relation) self.join_type = Arel::OuterJoin joining_relation.joins(self) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 69d5cd83f1..a248eb3a7b 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -43,7 +43,7 @@ module ActiveRecord end if attr_name == primary_key && attr_name != "id" - define_read_method(:id, attr_name, columns_hash[attr_name]) + define_read_method('id', attr_name, columns_hash[attr_name]) end end @@ -59,7 +59,9 @@ module ActiveRecord end # Define an attribute reader method. Cope with nil column. - def define_read_method(symbol, attr_name, column) + # method_name is the same as attr_name except when a non-standard primary key is used, + # we still define #id as an accessor for the key + def define_read_method(method_name, attr_name, column) cast_code = column.type_cast_code('v') access_code = "(v=@attributes['#{attr_name}']) && #{cast_code}" @@ -70,12 +72,25 @@ module ActiveRecord if cache_attribute?(attr_name) access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})" end - if symbol =~ /^[a-zA-Z_]\w*[!?=]?$/ - generated_attribute_methods.module_eval("def _#{symbol}; #{access_code}; end; alias #{symbol} _#{symbol}", __FILE__, __LINE__) + + # Where possible, generate the method by evalling a string, as this will result in + # faster accesses because it avoids the block eval and then string eval incurred + # by the second branch. + # + # The second, slower, branch is necessary to support instances where the database + # returns columns with extra stuff in (like 'my_column(omg)'). + if method_name =~ /^[a-zA-Z_]\w*[!?=]?$/ + generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + def _#{method_name} + #{access_code} + end + + alias #{method_name} _#{method_name} + STR else generated_attribute_methods.module_eval do - define_method("_#{symbol}") { eval(access_code) } - alias_method(symbol, "_#{symbol}") + define_method("_#{method_name}") { eval(access_code) } + alias_method(method_name, "_#{method_name}") end end end @@ -84,9 +99,11 @@ module ActiveRecord # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name) - send "_#{attr_name}" - rescue NoMethodError - _read_attribute attr_name + if respond_to? "_#{attr_name}" + send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s) + else + _read_attribute attr_name + end end def _read_attribute(attr_name) diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 6aac96df6f..62a3cfa9a5 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/class/attribute' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods @@ -58,7 +59,7 @@ module ActiveRecord private def create_time_zone_conversion_attribute?(name, column) - time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type) + time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp]) end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index fe81c7dc2f..08a41e2d8b 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -425,8 +425,8 @@ module ActiveRecord #:nodoc: self.store_full_sti_class = true # Stores the default scope for the class - class_attribute :default_scoping, :instance_writer => false - self.default_scoping = [] + class_attribute :default_scopes, :instance_writer => false + self.default_scopes = [] # Returns a hash of all the attributes that have been specified for serialization as # keys and their class restriction as values. @@ -870,7 +870,9 @@ module ActiveRecord #:nodoc: # Returns a scope for this class without taking into account the default_scope. # # class Post < ActiveRecord::Base - # default_scope :published => true + # def self.default_scope + # where :published => true + # end # end # # Post.all # Fires "SELECT * FROM posts WHERE published = true" @@ -892,13 +894,8 @@ module ActiveRecord #:nodoc: block_given? ? relation.scoping { yield } : relation end - def scoped_methods #:nodoc: - key = :"#{self}_scoped_methods" - Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup - end - def before_remove_const #:nodoc: - reset_scoped_methods + self.current_scope = nil end # Specifies how the record is loaded by +Marshal+. @@ -1020,7 +1017,7 @@ module ActiveRecord #:nodoc: super unless all_attributes_exists?(attribute_names) if match.finder? options = arguments.extract_options! - relation = options.any? ? construct_finder_arel(options, current_scoped_methods) : scoped + relation = options.any? ? scoped(options) : scoped relation.send :find_by_attributes, match, attribute_names, *arguments elsif match.instantiator? scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block @@ -1109,43 +1106,47 @@ module ActiveRecord #:nodoc: # end # # *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+. - def with_scope(method_scoping = {}, action = :merge, &block) - method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping) + def with_scope(scope = {}, action = :merge, &block) + # If another Active Record class has been passed in, get its current scope + scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope) + + previous_scope = self.current_scope - if method_scoping.is_a?(Hash) + if scope.is_a?(Hash) # Dup first and second level of hash (method and params). - method_scoping = method_scoping.dup - method_scoping.each do |method, params| - method_scoping[method] = params.dup unless params == true + scope = scope.dup + scope.each do |method, params| + scope[method] = params.dup unless params == true end - method_scoping.assert_valid_keys([ :find, :create ]) - relation = construct_finder_arel(method_scoping[:find] || {}) + scope.assert_valid_keys([ :find, :create ]) + relation = construct_finder_arel(scope[:find] || {}) + relation.default_scoped = true unless action == :overwrite - if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create] + if previous_scope && previous_scope.create_with_value && scope[:create] scope_for_create = if action == :merge - current_scoped_methods.create_with_value.merge(method_scoping[:create]) + previous_scope.create_with_value.merge(scope[:create]) else - method_scoping[:create] + scope[:create] end relation = relation.create_with(scope_for_create) else - scope_for_create = method_scoping[:create] - scope_for_create ||= current_scoped_methods.create_with_value if current_scoped_methods + scope_for_create = scope[:create] + scope_for_create ||= previous_scope.create_with_value if previous_scope relation = relation.create_with(scope_for_create) if scope_for_create end - method_scoping = relation + scope = relation end - method_scoping = current_scoped_methods.merge(method_scoping) if current_scoped_methods && action == :merge + scope = previous_scope.merge(scope) if previous_scope && action == :merge - self.scoped_methods << method_scoping + self.current_scope = scope begin yield ensure - self.scoped_methods.pop + self.current_scope = previous_scope end end @@ -1168,39 +1169,80 @@ MSG with_scope(method_scoping, :overwrite, &block) end - # Sets the default options for the model. The format of the - # <tt>options</tt> argument is the same as in find. + def current_scope #:nodoc: + Thread.current[:"#{self}_current_scope"] + end + + def current_scope=(scope) #:nodoc: + Thread.current[:"#{self}_current_scope"] = scope + end + + # Implement this method in your model to set a default scope for all operations on + # the model. # # class Person < ActiveRecord::Base - # default_scope order('last_name, first_name') + # def self.default_scope + # order('last_name, first_name') + # end # end # - # <tt>default_scope</tt> is also applied while creating/building a record. It is not + # Person.all # => SELECT * FROM people ORDER BY last_name, first_name + # + # The <tt>default_scope</tt> is also applied while creating/building a record. It is not # applied while updating a record. # # class Article < ActiveRecord::Base - # default_scope where(:published => true) + # def self.default_scope + # where(:published => true) + # end # end # # Article.new.published # => true # Article.create.published # => true - def default_scope(options = {}) - reset_scoped_methods - default_scoping = self.default_scoping.dup - self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop) - end + # + # === Deprecation warning + # + # There is an alternative syntax as follows: + # + # class Person < ActiveRecord::Base + # default_scope order('last_name, first_name') + # end + # + # This is now deprecated and will be removed in Rails 3.2. + def default_scope(scope = {}) + ActiveSupport::Deprecation.warn <<-WARN +Passing a hash or scope to default_scope is deprecated and will be removed in Rails 3.2. You should create a class method for your scope instead. For example, change this: - def current_scoped_methods #:nodoc: - method = scoped_methods.last - if method.respond_to?(:call) - relation.scoping { method.call } - else - method - end +class Post < ActiveRecord::Base + default_scope where(:published => true) +end + +To this: + +class Post < ActiveRecord::Base + def self.default_scope + where(:published => true) + end +end +WARN + + self.default_scopes = default_scopes.dup << scope end - def reset_scoped_methods #:nodoc: - Thread.current[:"#{self}_scoped_methods"] = nil + def build_default_scope #:nodoc: + if method(:default_scope).owner != Base.singleton_class + # Use relation.scoping to ensure we ignore whatever the current value of + # self.current_scope may be. + relation.scoping { 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) + else + default_scope.merge(scope) + end + end + end end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1916,11 +1958,8 @@ MSG end def populate_with_current_scope_attributes - if scope = self.class.send(:current_scoped_methods) - create_with = scope.scope_for_create - create_with.each { |att,value| - respond_to?("#{att}=") && send("#{att}=", value) - } + self.class.scoped.scope_for_create.each do |att,value| + respond_to?("#{att}=") && send("#{att}=", value) end end diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 86d58df99b..a175bf003c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -214,6 +214,24 @@ module ActiveRecord # needs to be aware of it because an ordinary +save+ will raise such exception # instead of quietly returning +false+. # + # == Debugging callbacks + # + # The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support + # <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property + # defines what part of the chain the callback runs in. + # + # To find all callbacks in the before_save callback chain: + # + # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) } + # + # Returns an array of callback objects that form the before_save chain. + # + # To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object: + # + # Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead) + # + # Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model. + # module Callbacks extend ActiveSupport::Concern 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 a3082b8f01..6d9b5c7b32 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -56,8 +56,17 @@ module ActiveRecord end # Returns the last auto-generated ID from the affected table. - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - insert_sql(sql, name, pk, id_value, sequence_name) + # + # +id_value+ will be returned unless the value is nil, in + # which case the database will attempt to calculate the last inserted + # id and return that value. + # + # 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) + value = exec_insert(sql, name, binds) + id_value || last_inserted_id(value) end # Executes the update statement and returns the number of rows affected. @@ -364,6 +373,15 @@ module ActiveRecord end end end + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + [sql, binds] + end + + def last_inserted_id(result) + row = result.rows.first + row && row.first + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 7489e88eef..3de850ec9e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -35,7 +35,43 @@ module ActiveRecord when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" else - "'#{quote_string(value.to_yaml)}'" + "'#{quote_string(YAML.dump(value))}'" + end + end + + # Cast a +value+ to a type that the database understands. For example, + # SQLite does not understand dates, so this method will convert a Date + # to a String. + def type_cast(value, column) + return value.id if value.respond_to?(:quoted_id) + + case value + when String, ActiveSupport::Multibyte::Chars + value = value.to_s + return value unless column + + case column.type + when :binary then value + when :integer then value.to_i + when :float then value.to_f + else + value + end + + when true, false + if column && column.type == :integer + value ? 1 : 0 + else + value ? 't' : 'f' + end + # BigDecimals need to be put in a non-normalized form and quoted. + when nil then nil + when BigDecimal then value.to_s('F') + when Numeric then value + when Date, Time then quoted_date(value) + when Symbol then value.to_s + else + YAML.dump(value) 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 0f44baa2fe..d24cce0a3c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -105,7 +105,7 @@ module ActiveRecord # Returns a bind substitution value given a +column+ and list of current # +binds+ - def substitute_for(column, binds) + def substitute_at(column, index) Arel.sql '?' end @@ -203,6 +203,10 @@ module ActiveRecord def release_savepoint end + def case_sensitive_modifier(node) + node + end + def current_savepoint_name "active_record_#{open_transactions}" end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 7bad511c64..7ac72acd58 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -282,6 +282,17 @@ module ActiveRecord end alias :create :insert_sql + def exec_insert(sql, name, binds) + binds = binds.dup + + # Pretend to support bind parameters + execute sql.gsub('?') { quote(*binds.shift.reverse) }, name + end + + def last_inserted_id(result) + @connection.last_id + end + def update_sql(sql, name = nil) super @connection.affected_rows @@ -528,6 +539,11 @@ module ActiveRecord def case_sensitive_equality_operator "= BINARY" end + deprecate :case_sensitive_equality_operator + + 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 @@ -587,8 +603,26 @@ module ActiveRecord # Returns an array of record hashes with the column names as keys and # column values as values. - def select(sql, name = nil) - execute(sql, name).each(:as => :hash) + def select(sql, name = nil, binds = []) + exec_query(sql, name, binds).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? diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e1186209d3..c2e75acb9a 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -196,6 +196,7 @@ module ActiveRecord @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} @statements = {} + @client_encoding = nil connect end @@ -243,6 +244,12 @@ module ActiveRecord 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 @@ -330,6 +337,63 @@ module ActiveRecord @statements.clear end + if "<3".respond_to?(:encode) + # Taken from here: + # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb + # Author: TOMITA Masahiro <tommy@tmtm.org> + ENCODINGS = { + "armscii8" => nil, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, + "macroman" => Encoding::MacRoman, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, + "utf8mb4" => Encoding::UTF_8, + } + else + ENCODINGS = Hash.new { |h,k| h[k] = k } + end + + # Get the client encoding for this database + def client_encoding + return @client_encoding if @client_encoding + + result = exec_query( + "SHOW VARIABLES WHERE Variable_name = 'character_set_client'", + 'SCHEMA') + @client_encoding = ENCODINGS[result.rows.last.last] + end + def exec_query(sql, name = 'SQL', binds = []) log(sql, name, binds) do result = nil @@ -345,7 +409,7 @@ module ActiveRecord end stmt.execute(*binds.map { |col, val| - col ? col.type_cast(val) : val + type_cast(val, col) }) if metadata = stmt.result_metadata cols = cache[:cols] ||= metadata.fetch_fields.map { |field| @@ -363,6 +427,14 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + + def last_inserted_id(result) + @connection.insert_id + end + def exec_without_stmt(sql, name = 'SQL') # :nodoc: # Some queries, like SHOW CREATE TABLE don't work through the prepared # statement API. For those queries, we need to use this method. :'( @@ -506,7 +578,7 @@ module ActiveRecord def tables(name = nil, database = nil) #:nodoc: tables = [] - result = execute(["SHOW TABLES", database].compact.join(' IN '), name) + result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA') result.each { |field| tables << field[0] } result.free tables @@ -551,7 +623,7 @@ module ActiveRecord def columns(table_name, name = nil)#:nodoc: sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" columns = [] - result = execute(sql) + result = execute(sql, 'SCHEMA') result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } result.free columns @@ -638,7 +710,7 @@ module ActiveRecord # Returns a table's primary key and belonging sequence. def pk_and_sequence_for(table) #:nodoc: keys = [] - result = execute("describe #{quote_table_name(table)}") + result = execute("describe #{quote_table_name(table)}", 'SCHEMA') result.each_hash do |h| keys << h["Field"]if h["Key"] == "PRI" end @@ -655,6 +727,11 @@ module ActiveRecord def case_sensitive_equality_operator "= BINARY" end + deprecate :case_sensitive_equality_operator + + 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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5a830a50fb..e74ec84e81 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -229,7 +229,12 @@ module ActiveRecord @statements = {} connect - @local_tz = execute('SHOW TIME ZONE').first["TimeZone"] + + if postgresql_version < 80200 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" + end + + @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end def clear_cache! @@ -293,13 +298,13 @@ module ActiveRecord # Enable standard-conforming strings if available. def set_standard_conforming_strings old, self.client_min_messages = client_min_messages, 'panic' - execute('SET standard_conforming_strings = on') rescue nil + execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil ensure self.client_min_messages = old end def supports_insert_with_returning? - postgresql_version >= 80200 + true end def supports_ddl_transactions? @@ -310,10 +315,9 @@ module ActiveRecord true end - # Returns the configured supported identifier length supported by PostgreSQL, - # or report the default of 63 on PostgreSQL 7.x. + # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63) + @table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i end # QUOTING ================================================== @@ -356,6 +360,18 @@ module ActiveRecord end end + def type_cast(value, column) + return super unless column + + case value + when String + return super unless 'bytea' == column.sql_type + escape_bytea(value) + else + super + end + end + # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -404,7 +420,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def supports_disable_referential_integrity?() #:nodoc: - postgresql_version >= 80100 + true end def disable_referential_integrity #:nodoc: @@ -427,34 +443,16 @@ module ActiveRecord end # Executes an INSERT query and returns the new record's ID - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) + def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) # Extract the table from the insert sql. Yuck. - table = sql.split(" ", 4)[2].gsub('"', '') - - # Try an insert with 'returning id' if available (PG >= 8.2) - if supports_insert_with_returning? - pk, sequence_name = *pk_and_sequence_for(table) unless pk - if pk - id = select_value("#{sql} RETURNING #{quote_column_name(pk)}") - clear_query_cache - return id - end - end + _, table = extract_schema_and_table(sql.split(" ", 4)[2]) - # Otherwise, insert then grab last_insert_id. - if insert_id = super - insert_id - else - # If neither pk nor sequence name is given, look them up. - unless pk || sequence_name - pk, sequence_name = *pk_and_sequence_for(table) - end + pk ||= primary_key(table) - # If a pk is given, fallback to default sequence name. - # Don't fetch last insert id for a table without a pk. - if pk && sequence_name ||= default_sequence_name(table, pk) - last_insert_id(sequence_name) - end + if pk + select_value("#{sql} RETURNING #{quote_column_name(pk)}") + else + super end end alias :create :insert @@ -525,8 +523,8 @@ module ActiveRecord end end - def substitute_for(column, current_values) - Arel.sql("$#{current_values.length + 1}") + def substitute_at(column, index) + Arel.sql("$#{index + 1}") end def exec_query(sql, name = 'SQL', binds = []) @@ -544,7 +542,7 @@ module ActiveRecord # Clear the queue @connection.get_last_result @connection.send_query_prepared(key, binds.map { |col, val| - col ? col.type_cast(val) : val + type_cast(val, col) }) @connection.block result = @connection.get_last_result @@ -554,6 +552,22 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + + def sql_for_insert(sql, pk, id_value, sequence_name, binds) + unless pk + _, table = extract_schema_and_table(sql.split(" ", 4)[2]) + + pk = primary_key(table) + end + + sql = "#{sql} RETURNING #{quote_column_name(pk)}" if pk + + [sql, binds] + end + # Executes an UPDATE query and returns the number of affected tuples. def update_sql(sql, name = nil) super.cmd_tuples @@ -632,20 +646,12 @@ module ActiveRecord # Example: # drop_database 'matt_development' def drop_database(name) #:nodoc: - if postgresql_version >= 80200 - execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" - else - begin - execute "DROP DATABASE #{quote_table_name(name)}" - rescue ActiveRecord::StatementInvalid - @logger.warn "#{name} database doesn't exist." if @logger - end - end + execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" end # Returns the list of all tables in the schema search path or a specified schema. def tables(name = nil) - query(<<-SQL, name).map { |row| row[0] } + query(<<-SQL, 'SCHEMA').map { |row| row[0] } SELECT tablename FROM pg_tables WHERE schemaname = ANY (current_schemas(false)) @@ -653,7 +659,21 @@ module ActiveRecord end def table_exists?(name) - name = name.to_s + schema, table = extract_schema_and_table(name.to_s) + + binds = [[nil, table.gsub(/(^"|"$)/,'')]] + binds << [nil, schema] if schema + + exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0 + SELECT COUNT(*) + FROM pg_tables + WHERE tablename = $1 + #{schema ? "AND schemaname = $2" : ''} + SQL + end + + # Extracts the table and schema name from +name+ + def extract_schema_and_table(name) schema, table = name.split('.', 2) unless table # A table was provided without a schema @@ -665,13 +685,7 @@ module ActiveRecord table = name schema = nil end - - query(<<-SQL).first[0].to_i > 0 - SELECT COUNT(*) - FROM pg_tables - WHERE tablename = '#{table.gsub(/(^"|"$)/,'')}' - #{schema ? "AND schemaname = '#{schema}'" : ''} - SQL + [schema, table] end # Returns the list of all indexes for a table. @@ -748,37 +762,47 @@ module ActiveRecord # Returns the current client message level. def client_min_messages - query('SHOW client_min_messages')[0][0] + query('SHOW client_min_messages', 'SCHEMA')[0][0] end # Set the client message level. def client_min_messages=(level) - execute("SET client_min_messages TO '#{level}'") + execute("SET client_min_messages TO '#{level}'", 'SCHEMA') end # Returns the sequence name for a table's primary key or some other specified key. def default_sequence_name(table_name, pk = nil) #:nodoc: - default_pk, default_seq = pk_and_sequence_for(table_name) - default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq" + serial_sequence(table_name, pk || 'id').split('.').last + rescue ActiveRecord::StatementInvalid + "#{table_name}_#{pk || 'id'}_seq" + end + + def serial_sequence(table, column) + result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]]) + SELECT pg_get_serial_sequence($1, $2) + eosql + result.rows.first.first end # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: unless pk and sequence default_pk, default_sequence = pk_and_sequence_for(table) + pk ||= default_pk sequence ||= default_sequence end - if pk - if sequence - quoted_sequence = quote_column_name(sequence) - select_value <<-end_sql, 'Reset sequence' - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) - end_sql - else - @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger - end + if @logger && pk && !sequence + @logger.warn "#{table} has primary key #{pk} with no default sequence" + end + + if pk && sequence + quoted_sequence = quote_column_name(sequence) + + select_value <<-end_sql, 'Reset sequence' + SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + end_sql end end @@ -786,7 +810,7 @@ module ActiveRecord def pk_and_sequence_for(table) #:nodoc: # First try looking for a sequence with a dependency on the # given table's primary key. - result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first + result = exec_query(<<-end_sql, 'SCHEMA').rows.first SELECT attr.attname, seq.relname FROM pg_class seq, pg_attribute attr, @@ -803,28 +827,6 @@ module ActiveRecord AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql - if result.nil? or result.empty? - # If that fails, try parsing the primary key's default value. - # Support the 7.x and 8.0 nextval('foo'::text) as well as - # the 8.1+ nextval('foo'::regclass). - result = query(<<-end_sql, 'PK and custom sequence')[0] - SELECT attr.attname, - CASE - WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN - substr(split_part(def.adsrc, '''', 2), - strpos(split_part(def.adsrc, '''', 2), '.')+1) - ELSE split_part(def.adsrc, '''', 2) - END - FROM pg_class t - JOIN pg_attribute attr ON (t.oid = attrelid) - JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) - JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) - WHERE t.oid = '#{quote_table_name(table)}'::regclass - AND cons.contype = 'p' - AND def.adsrc ~* 'nextval' - end_sql - end - # [primary_key, sequence] [result.first, result.last] rescue @@ -833,8 +835,21 @@ module ActiveRecord # 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 + row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first + SELECT DISTINCT(attr.attname) + FROM pg_attribute attr, + pg_depend dep, + pg_namespace name, + pg_constraint cons + WHERE attr.attrelid = dep.refobjid + AND attr.attnum = dep.refobjsubid + AND attr.attrelid = cons.conrelid + AND attr.attnum = cons.conkey[1] + AND cons.contype = 'p' + AND dep.refobjid = $1::regclass + end_sql + + row && row.first end # Renames a table. @@ -848,38 +863,14 @@ module ActiveRecord add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" add_column_options!(add_column_sql, options) - begin - execute add_column_sql - rescue ActiveRecord::StatementInvalid => e - raise e if postgresql_version > 80000 - - execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}") - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) - change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) - end + execute add_column_sql end # Changes the column of a table. def change_column(table_name, column_name, type, options = {}) quoted_table_name = quote_table_name(table_name) - begin - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" - rescue ActiveRecord::StatementInvalid => e - raise e if postgresql_version > 80000 - # This is PostgreSQL 7.x, so we have to use a more arcane way of doing it. - begin - begin_db_transaction - tmp_column_name = "#{column_name}_ar_tmp" - add_column(table_name, tmp_column_name, type, options) - execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})" - remove_column(table_name, column_name) - rename_column(table_name, tmp_column_name, column_name) - commit_db_transaction - rescue - rollback_db_transaction - end - end + execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}" change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -1031,9 +1022,9 @@ module ActiveRecord # If using Active Record's time zone support configure the connection to return # TIMESTAMP WITH ZONE types in UTC. if ActiveRecord::Base.default_timezone == :utc - execute("SET time zone 'UTC'") + execute("SET time zone 'UTC'", 'SCHEMA') elsif @local_tz - execute("SET time zone '#{@local_tz}'") + execute("SET time zone '#{@local_tz}'", 'SCHEMA') end end @@ -1076,7 +1067,7 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) #:nodoc: - exec_query(<<-end_sql).rows + exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index 32229a8410..9e7f874f4b 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -165,7 +165,7 @@ module ActiveRecord cols = cache[:cols] ||= stmt.columns stmt.reset! stmt.bind_params binds.map { |col, val| - col ? col.type_cast(val) : val + type_cast(val, col) } end @@ -173,6 +173,14 @@ module ActiveRecord end end + def exec_insert(sql, name, binds) + exec_query(sql, name, binds) + end + + def last_inserted_id(result) + @connection.last_insert_row_id + end + def execute(sql, name = nil) #:nodoc: log(sql, name) { @connection.execute(sql) } end @@ -188,7 +196,8 @@ module ActiveRecord end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super || @connection.last_insert_row_id + super + id_value || @connection.last_insert_row_id end alias :create :insert_sql diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index d523c643ba..0939ec2626 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -173,10 +173,10 @@ class FixturesFileNotFound < StandardError; end # 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 +# = 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 or CSV fixtures to create a bunch of fixtures for load testing, like: +# mix ERB in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like: # # <% for i in 1..1000 %> # fix_<%= i %>: @@ -186,7 +186,7 @@ class FixturesFileNotFound < StandardError; 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>. +# 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 diff --git a/activerecord/lib/active_record/identity_map.rb b/activerecord/lib/active_record/identity_map.rb index d18b2b0a54..95a8e5cff7 100644 --- a/activerecord/lib/active_record/identity_map.rb +++ b/activerecord/lib/active_record/identity_map.rb @@ -50,7 +50,14 @@ module ActiveRecord def get(klass, primary_key) obj = repository[klass.symbolized_base_class][primary_key] - obj.is_a?(klass) ? obj : nil + if obj.is_a?(klass) + if ActiveRecord::Base.logger + ActiveRecord::Base.logger.debug "#{klass} with ID = #{primary_key} loaded from Identity Map" + end + obj + else + nil + end end def add(record) diff --git a/activerecord/lib/active_record/named_scope.rb b/activerecord/lib/active_record/named_scope.rb index d291632260..f1df04950b 100644 --- a/activerecord/lib/active_record/named_scope.rb +++ b/activerecord/lib/active_record/named_scope.rb @@ -9,11 +9,6 @@ module ActiveRecord module NamedScope extend ActiveSupport::Concern - included do - class_attribute :scopes - self.scopes = {} - end - module ClassMethods # Returns an anonymous \scope. # @@ -35,7 +30,13 @@ module ActiveRecord if options scoped.apply_finder_options(options) else - current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone + if current_scope + current_scope.clone + else + scope = relation.clone + scope.default_scoped = true + scope + end end end @@ -50,6 +51,14 @@ module ActiveRecord # The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red, # in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>. # + # Note that this is simply 'syntactic sugar' for defining an actual class method: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red') + # end + # end + # # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it # resembles the association object constructed by a <tt>has_many</tt> declaration. For instance, # you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>. @@ -73,14 +82,34 @@ module ActiveRecord # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean # only shirts. # - # Named \scopes can also be procedural: + # If you need to pass parameters to a scope, define it as a normal method: # # class Shirt < ActiveRecord::Base - # scope :colored, lambda {|color| where(:color => color) } + # def self.colored(color) + # where(:color => color) + # end # end # # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts. # + # Note that scopes defined with \scope will be evaluated when they are defined, rather than + # when they are used. For example, the following would be incorrect: + # + # class Post < ActiveRecord::Base + # scope :recent, where('published_at >= ?', Time.now - 1.week) + # end + # + # The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt> + # class was defined, and so the resultant SQL query would always be the same. The correct + # way to do this would be via a class method, which will re-evaluate the scope each time + # it is called: + # + # class Post < ActiveRecord::Base + # def self.recent + # where('published_at >= ?', Time.now - 1.week) + # end + # end + # # Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations: # # class Shirt < ActiveRecord::Base @@ -91,6 +120,18 @@ module ActiveRecord # end # end # + # The above could also be written as a class method like so: + # + # class Shirt < ActiveRecord::Base + # def self.red + # where(:color => 'red').extending do + # def dom_id + # 'red_shirts' + # end + # end + # end + # end + # # Scopes can also be used while creating/building a record. # # class Article < ActiveRecord::Base @@ -99,34 +140,68 @@ module ActiveRecord # # Article.published.new.published # => true # Article.published.create.published # => true + # + # Class methods on your model are automatically available + # on scopes. Assuming the following setup: + # + # class Article < ActiveRecord::Base + # scope :published, where(:published => true) + # scope :featured, where(:featured => true) + # + # def self.latest_article + # order('published_at desc').first + # end + # + # def self.titles + # map(&:title) + # end + # + # end + # + # We are able to call the methods like this: + # + # Article.published.featured.latest_article + # Article.featured.titles + def scope(name, scope_options = {}) name = name.to_sym valid_scope_name?(name) extension = Module.new(&Proc.new) if block_given? + if !scope_options.is_a?(Relation) && scope_options.respond_to?(:call) + ActiveSupport::Deprecation.warn <<-WARN +Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your scope to be lazily evaluated, or takes parameters, please define it as a normal class method instead. For example, change this: + +class Post < ActiveRecord::Base + scope :unpublished, lambda { where('published_at > ?', Time.now) } +end + +To this: + +class Post < ActiveRecord::Base + def self.unpublished + where('published_at > ?', Time.now) + end +end + WARN + end + scope_proc = lambda do |*args| options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options + options = scoped.apply_finder_options(options) if options.is_a?(Hash) - relation = if options.is_a?(Hash) - scoped.apply_finder_options(options) - elsif options - scoped.merge(options) - else - scoped - end + relation = scoped.merge(options) extension ? relation.extending(extension) : relation end - self.scopes = self.scopes.merge name => scope_proc - - singleton_class.send(:redefine_method, name, &scopes[name]) + singleton_class.send(:redefine_method, name, &scope_proc) end protected def valid_scope_name?(name) - if !scopes[name] && respond_to?(name, true) + if respond_to?(name, true) logger.warn "Creating scope :#{name}. " \ "Overwriting existing method #{self.name}.#{name}." end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 522c0cfc9f..08b27b6a8e 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -403,12 +403,6 @@ module ActiveRecord unless reject_new_record?(association_name, attributes) association.build(attributes.except(*UNASSIGNABLE_KEYS)) end - elsif existing_records.count == 0 #Existing record but not yet associated - existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id']) - if !call_reject_if(association_name, attributes) - association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded? - assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) - end elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s } unless association.loaded? || call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's @@ -452,6 +446,7 @@ module ActiveRecord end def call_reject_if(association_name, attributes) + return false if has_destroy_flag?(attributes) case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index ff36814684..6b3c38cb58 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/inclusion' + db_namespace = namespace :db do task :load_config => :rails_env do require 'active_record' @@ -135,7 +137,7 @@ db_namespace = namespace :db do end def local_database?(config, &block) - if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank? + if config['host'].in?(["127.0.0.1", "localhost"]) || config['host'].blank? yield else $stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host." diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e801bc4afa..bcba85d7a4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/module/deprecation' +require 'active_support/core_ext/object/inclusion' module ActiveRecord # = Active Record Reflection @@ -163,7 +164,7 @@ module ActiveRecord def initialize(macro, name, options, active_record) super - @collection = [:has_many, :has_and_belongs_to_many].include?(macro) + @collection = macro.in?([:has_many, :has_and_belongs_to_many]) end # Returns a new, unsaved instance of the associated class. +options+ will diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 896daf516e..359f9d8a66 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,7 +6,7 @@ module ActiveRecord JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind] - SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] + SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches @@ -15,14 +15,16 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass attr_reader :table, :klass, :loaded - attr_accessor :extensions + attr_accessor :extensions, :default_scoped alias :loaded? :loaded + alias :default_scoped? :default_scoped def initialize(klass, table) @klass, @table = klass, table @implicit_readonly = nil @loaded = false + @default_scoped = false SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)} (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])} @@ -46,17 +48,30 @@ module ActiveRecord im = arel.create_insert im.into @table + conn = @klass.connection + + substitutes = values.sort_by { |arel_attr,_| arel_attr.name } + binds = substitutes.map do |arel_attr, value| + [@klass.columns_hash[arel_attr.name], value] + end + + substitutes.each_with_index do |tuple, i| + tuple[1] = conn.substitute_at(tuple.first, i) + end + if values.empty? # empty insert im.values = im.create_values [connection.null_insert_value], [] else - im.insert values + im.insert substitutes end - @klass.connection.insert( + conn.insert( im.to_sql, 'SQL', primary_key, - primary_key_value) + primary_key_value, + nil, + binds) end def new(*args, &block) @@ -154,12 +169,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - @klass.scoped_methods << self - begin - yield - ensure - @klass.scoped_methods.pop - end + @klass.send(:with_scope, self, :overwrite) { yield } end # Updates all records with details given if they match a set of conditions supplied, limits and order can @@ -194,6 +204,7 @@ module ActiveRecord # # The same idea applies to limit and order # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David') def update_all(updates, conditions = nil, options = {}) + IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled? if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else @@ -322,6 +333,7 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. def delete_all(conditions = nil) + IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled? if conditions where(conditions).delete_all else @@ -353,6 +365,7 @@ module ActiveRecord # # Delete multiple rows # Todo.delete([2,3,4]) def delete(id_or_array) + IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled? where(primary_key => id_or_array).delete_all end @@ -374,7 +387,7 @@ module ActiveRecord end def where_values_hash - equalities = @where_values.grep(Arel::Nodes::Equality).find_all { |node| + equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node| node.left.relation.name == table_name } @@ -402,13 +415,20 @@ module ActiveRecord to_a.inspect end + def with_default_scope #:nodoc: + if default_scoped? + default_scope = @klass.send(:build_default_scope) + default_scope ? default_scope.merge(self) : self + else + self + end + end + protected def method_missing(method, *args, &block) if Array.method_defined?(method) to_a.send(method, *args, &block) - elsif @klass.scopes[method] - merge(@klass.send(method, *args, &block)) elsif @klass.respond_to?(method) scoping { @klass.send(method, *args, &block) } elsif arel.respond_to?(method) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8fa315bdf3..aae257a0e7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -309,9 +309,18 @@ module ActiveRecord def find_one(id) id = id.id if ActiveRecord::Base === id + if IdentityMap.enabled? && where_values.blank? && + limit_value.blank? && order_values.blank? && + includes_values.blank? && preload_values.blank? && + readonly_value.nil? && joins_values.blank? && + !@klass.locking_enabled? && + record = IdentityMap.get(@klass, id) + return record + end + column = columns_hash[primary_key] - substitute = connection.substitute_for(column, @bind_values) + substitute = connection.substitute_at(column, @bind_values.length) relation = where(table[primary_key].eq(substitute)) relation.bind_values = [[column, id]] record = relation.first @@ -343,8 +352,8 @@ module ActiveRecord if result.size == expected_size result else - conditions = arel.wheres.map { |x| x.value }.join(', ') - conditions = " [WHERE #{conditions}]" if conditions.present? + conditions = arel.where_sql + conditions = " [#{conditions}]" if conditions error = "Couldn't find all #{@klass.name.pluralize} with IDs " error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})" diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 02b7056989..94aa999715 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -8,7 +8,8 @@ module ActiveRecord attr_accessor :includes_values, :eager_load_values, :preload_values, :select_values, :group_values, :order_values, :joins_values, :where_values, :having_values, :bind_values, - :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value + :limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, + :from_value, :reorder_value def includes(*args) args.reject! {|a| a.blank? } @@ -63,7 +64,11 @@ module ActiveRecord end def reorder(*args) - except(:order).order(args) + return self if args.blank? + + relation = clone + relation.reorder_value = args.flatten + relation end def joins(*args) @@ -163,7 +168,7 @@ module ActiveRecord end def arel - @arel ||= build_arel + @arel ||= with_default_scope.build_arel end def build_arel @@ -180,7 +185,8 @@ module ActiveRecord arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty? - arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty? + order = @reorder_value ? @reorder_value : @order_values + arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty? build_select(arel, @select_values.uniq) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 128e0fbd86..69706b5ead 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -8,6 +8,8 @@ module ActiveRecord merged_relation = clone + r = r.with_default_scope if r.default_scoped? && r.klass != klass + Relation::ASSOCIATION_METHODS.each do |method| value = r.send(:"#{method}_values") @@ -70,6 +72,7 @@ module ActiveRecord # def except(*skips) result = self.class.new(@klass, table) + result.default_scoped = default_scoped ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method| result.send(:"#{method}_values=", send(:"#{method}_values")) @@ -94,6 +97,7 @@ module ActiveRecord # def only(*onlies) result = self.class.new(@klass, table) + result.default_scoped = default_scoped ((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method| result.send(:"#{method}_values=", send(:"#{method}_values")) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 0465b21e88..243012f88c 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -1,9 +1,9 @@ module ActiveRecord ### - # This class encapsulates a Result returned from calling +exec+ on any + # This class encapsulates a Result returned from calling +exec_query+ on any # database connection adapter. For example: # - # x = ActiveRecord::Base.connection.exec('SELECT * FROM foo') + # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo') # x # => #<ActiveRecord::Result:0xdeadbeef> class Result include Enumerable diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 60d4c256c4..d363f36108 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -270,6 +270,7 @@ module ActiveRecord def rolledback!(force_restore_state = false) #:nodoc: run_callbacks :rollback ensure + IdentityMap.remove(self) if IdentityMap.enabled? restore_transaction_record_state(force_restore_state) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 9cd6c26322..d1225a9ed9 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,6 +14,7 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) + table = finder_class.arel_table coder = record.class.serialized_attributes[attribute.to_s] @@ -21,21 +22,15 @@ module ActiveRecord value = coder.dump value end - sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value) - - relation = finder_class.unscoped.where(sql, *params) + relation = build_relation(finder_class, table, attribute, value) + relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted? Array.wrap(options[:scope]).each do |scope_item| scope_value = record.send(scope_item) - relation = relation.where(scope_item => scope_value) - end - - if record.persisted? - # TODO : This should be in Arel - relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id)) + relation = relation.and(table[scope_item].eq(scope_value)) end - if relation.exists? + if finder_class.unscoped.where(relation).exists? record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value)) end end @@ -57,27 +52,18 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc: + def build_relation(klass, table, attribute, value) #:nodoc: column = klass.columns_hash[attribute.to_s] + value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? - operator = if value.nil? - "IS ?" - elsif column.text? - value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s - "#{klass.connection.case_sensitive_equality_operator} ?" - else - "= ?" - end - - sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}" - - if value.nil? || (options[:case_sensitive] || !column.text?) - sql = "#{sql_attribute} #{operator}" + if !options[:case_sensitive] && column.text? + relation = table[attribute].matches(value) else - sql = "LOWER(#{sql_attribute}) = LOWER(?)" + value = klass.connection.case_sensitive_modifier(value) + relation = table[attribute].eq(value) end - [sql, [value]] + relation end end diff --git a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb index afcda2a98a..90923f6e74 100644 --- a/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/session_migration/session_migration_generator.rb @@ -1,4 +1,5 @@ require 'rails/generators/active_record' +require 'active_support/core_ext/object/inclusion' module ActiveRecord module Generators @@ -13,7 +14,7 @@ module ActiveRecord def session_table_name current_table_name = ActiveRecord::SessionStore::Session.table_name - if ["sessions", "session"].include?(current_table_name) + if current_table_name.in?(["sessions", "session"]) current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session') end current_table_name diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index eb3f8143e7..eee771ecff 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -44,7 +44,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end def test_bind_value_substitute - bind_param = @connection.substitute_for('foo', []) + bind_param = @connection.substitute_at('foo', 0) assert_equal Arel.sql('?'), bind_param end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb new file mode 100644 index 0000000000..146b77a95c --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -0,0 +1,69 @@ +# encoding: utf-8 + +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MysqlAdapterTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + @conn.exec_query('drop table if exists ex') + @conn.exec_query(<<-eosql) + CREATE TABLE `ex` ( + `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY, + `number` integer, + `data` varchar(255)) + eosql + end + + def test_client_encoding + if "<3".respond_to?(:encoding) + assert_equal Encoding::UTF_8, @conn.client_encoding + else + assert_equal 'utf8', @conn.client_encoding + end + end + + def test_exec_insert_number + insert(@conn, 'number' => 10) + + result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') + + assert_equal 1, result.rows.length + assert_equal 10, result.rows.last.last + end + + def test_exec_insert_string + str = 'いただきます!' + insert(@conn, 'number' => 10, 'data' => str) + + result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') + + value = result.rows.last.last + + if "<3".respond_to?(:encoding) + # FIXME: this should probably be inside the mysql AR adapter? + value.force_encoding(@conn.client_encoding) + + # The strings in this file are utf-8, so transcode to utf-8 + value.encode!(Encoding::UTF_8) + end + + assert_equal str, value + end + + private + def insert(ctx, data) + binds = data.map { |name, value| + [ctx.columns('ex').find { |x| x.name == name }, value] + } + columns = binds.map(&:first).map(&:name) + + sql = "INSERT INTO ex (#{columns.join(", ")}) + VALUES (#{(['?'] * columns.length).join(', ')})" + + ctx.exec_insert(sql, 'SQL', binds) + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb new file mode 100644 index 0000000000..9673e2bb46 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MysqlAdapter + class QuotingTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_true + c = Column.new(nil, 1, 'boolean') + assert_equal 1, @conn.type_cast(true, nil) + assert_equal 1, @conn.type_cast(true, c) + end + + def test_type_cast_false + c = Column.new(nil, 1, 'boolean') + assert_equal 0, @conn.type_cast(false, nil) + assert_equal 0, @conn.type_cast(false, c) + end + end + end + end +end + diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index b0a4a4e39d..7c49236854 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require "cases/helper" module ActiveRecord @@ -6,7 +7,52 @@ module ActiveRecord def setup @connection = ActiveRecord::Base.connection @connection.exec_query('drop table if exists ex') - @connection.exec_query('create table ex(id serial primary key, data character varying(255))') + @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))') + end + + def test_serial_sequence + assert_equal 'public.accounts_id_seq', + @connection.serial_sequence('accounts', 'id') + + assert_raises(ActiveRecord::StatementInvalid) do + @connection.serial_sequence('zomg', 'id') + end + end + + def test_default_sequence_name + assert_equal 'accounts_id_seq', + @connection.default_sequence_name('accounts', 'id') + + assert_equal 'accounts_id_seq', + @connection.default_sequence_name('accounts') + end + + def test_default_sequence_name_bad_table + assert_equal 'zomg_id_seq', + @connection.default_sequence_name('zomg', 'id') + + assert_equal 'zomg_id_seq', + @connection.default_sequence_name('zomg') + end + + def test_exec_insert_number + insert(@connection, 'number' => 10) + + result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') + + assert_equal 1, result.rows.length + assert_equal "10", result.rows.last.last + end + + def test_exec_insert_string + str = 'いただきます!' + insert(@connection, 'number' => 10, 'data' => str) + + result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') + + value = result.rows.last.last + + assert_equal str, value end def test_table_alias_length @@ -56,13 +102,28 @@ module ActiveRecord assert_equal [['1', 'foo']], result.rows end - def test_substitute_for - bind = @connection.substitute_for(nil, []) + def test_substitute_at + bind = @connection.substitute_at(nil, 0) assert_equal Arel.sql('$1'), bind - bind = @connection.substitute_for(nil, [nil]) + bind = @connection.substitute_at(nil, 1) assert_equal Arel.sql('$2'), bind end + + private + def insert(ctx, data) + binds = data.map { |name, value| + [ctx.columns('ex').find { |x| x.name == name }, value] + } + columns = binds.map(&:first).map(&:name) + + bind_subs = columns.length.times.map { |x| "$#{x + 1}" } + + sql = "INSERT INTO ex (#{columns.join(", ")}) + VALUES (#{bind_subs.join(', ')})" + + ctx.exec_insert(sql, 'SQL', binds) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb new file mode 100644 index 0000000000..172055f15c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class QuotingTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_true + c = Column.new(nil, 1, 'boolean') + assert_equal 't', @conn.type_cast(true, nil) + assert_equal 't', @conn.type_cast(true, c) + end + + def test_type_cast_false + c = Column.new(nil, 1, 'boolean') + assert_equal 'f', @conn.type_cast(false, nil) + assert_equal 'f', @conn.type_cast(false, c) + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb b/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb deleted file mode 100644 index d1fc470907..0000000000 --- a/activerecord/test/cases/adapters/sqlite/sqlite_adapter_test.rb +++ /dev/null @@ -1,229 +0,0 @@ -# encoding: utf-8 -require "cases/helper" -require 'models/binary' - -module ActiveRecord - module ConnectionAdapters - class SQLiteAdapterTest < ActiveRecord::TestCase - class DualEncoding < ActiveRecord::Base - end - - def setup - @ctx = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => nil - @ctx.execute <<-eosql - CREATE TABLE items ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - end - - def test_quote_binary_column_escapes_it - DualEncoding.connection.execute(<<-eosql) - CREATE TABLE dual_encodings ( - id integer PRIMARY KEY AUTOINCREMENT, - name string, - data binary - ) - eosql - str = "\x80".force_encoding("ASCII-8BIT") - binary = DualEncoding.new :name => 'いただきます!', :data => str - binary.save! - assert_equal str, binary.data - end - - def test_execute - @ctx.execute "INSERT INTO items (number) VALUES (10)" - records = @ctx.execute "SELECT * FROM items" - assert_equal 1, records.length - - record = records.first - assert_equal 10, record['number'] - assert_equal 1, record['id'] - end - - def test_quote_string - assert_equal "''", @ctx.quote_string("'") - end - - def test_insert_sql - 2.times do |i| - rv = @ctx.insert_sql "INSERT INTO items (number) VALUES (#{i})" - assert_equal(i + 1, rv) - end - - records = @ctx.execute "SELECT * FROM items" - assert_equal 2, records.length - end - - def test_insert_sql_logged - sql = "INSERT INTO items (number) VALUES (10)" - name = "foo" - - assert_logged([[sql, name]]) do - @ctx.insert_sql sql, name - end - end - - def test_insert_id_value_returned - sql = "INSERT INTO items (number) VALUES (10)" - idval = 'vuvuzela' - id = @ctx.insert_sql sql, nil, nil, idval - assert_equal idval, id - end - - def test_select_rows - 2.times do |i| - @ctx.create "INSERT INTO items (number) VALUES (#{i})" - end - rows = @ctx.select_rows 'select number, id from items' - assert_equal [[0, 1], [1, 2]], rows - end - - def test_select_rows_logged - sql = "select * from items" - name = "foo" - - assert_logged([[sql, name]]) do - @ctx.select_rows sql, name - end - end - - def test_transaction - count_sql = 'select count(*) from items' - - @ctx.begin_db_transaction - @ctx.create "INSERT INTO items (number) VALUES (10)" - - assert_equal 1, @ctx.select_rows(count_sql).first.first - @ctx.rollback_db_transaction - assert_equal 0, @ctx.select_rows(count_sql).first.first - end - - def test_tables - assert_equal %w{ items }, @ctx.tables - - @ctx.execute <<-eosql - CREATE TABLE people ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer - ) - eosql - assert_equal %w{ items people }.sort, @ctx.tables.sort - end - - def test_tables_logs_name - name = "hello" - assert_logged [[name]] do - @ctx.tables(name) - assert_not_nil @ctx.logged.first.shift - end - end - - def test_columns - columns = @ctx.columns('items').sort_by { |x| x.name } - assert_equal 2, columns.length - assert_equal %w{ id number }.sort, columns.map { |x| x.name } - assert_equal [nil, nil], columns.map { |x| x.default } - assert_equal [true, true], columns.map { |x| x.null } - end - - def test_columns_with_default - @ctx.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer default 10 - ) - eosql - column = @ctx.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert_equal 10, column.default - end - - def test_columns_with_not_null - @ctx.execute <<-eosql - CREATE TABLE columns_with_default ( - id integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - column = @ctx.columns('columns_with_default').find { |x| - x.name == 'number' - } - assert !column.null, "column should not be null" - end - - def test_indexes_logs - intercept_logs_on @ctx - assert_difference('@ctx.logged.length') do - @ctx.indexes('items') - end - assert_match(/items/, @ctx.logged.last.first) - end - - def test_no_indexes - assert_equal [], @ctx.indexes('items') - end - - def test_index - @ctx.add_index 'items', 'id', :unique => true, :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - - assert_equal 'items', index.table - assert index.unique, 'index is unique' - assert_equal ['id'], index.columns - end - - def test_non_unique_index - @ctx.add_index 'items', 'id', :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - assert !index.unique, 'index is not unique' - end - - def test_compound_index - @ctx.add_index 'items', %w{ id number }, :name => 'fun' - index = @ctx.indexes('items').find { |idx| idx.name == 'fun' } - assert_equal %w{ id number }.sort, index.columns.sort - end - - def test_primary_key - assert_equal 'id', @ctx.primary_key('items') - - @ctx.execute <<-eosql - CREATE TABLE foos ( - internet integer PRIMARY KEY AUTOINCREMENT, - number integer not null - ) - eosql - assert_equal 'internet', @ctx.primary_key('foos') - end - - def test_no_primary_key - @ctx.execute 'CREATE TABLE failboat (number integer not null)' - assert_nil @ctx.primary_key('failboat') - end - - private - - def assert_logged logs - intercept_logs_on @ctx - yield - assert_equal logs, @ctx.logged - end - - def intercept_logs_on ctx - @ctx.extend(Module.new { - attr_accessor :logged - def log sql, name - @logged << [sql, name] - yield - end - }) - @ctx.logged = [] - end - end - end -end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb new file mode 100644 index 0000000000..0d9db92447 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -0,0 +1,93 @@ +require "cases/helper" +require 'bigdecimal' +require 'yaml' + +module ActiveRecord + module ConnectionAdapters + class SQLiteAdapter + class QuotingTest < ActiveRecord::TestCase + def setup + @conn = Base.sqlite3_connection :database => ':memory:', + :adapter => 'sqlite3', + :timeout => 100 + end + + def test_type_cast_symbol + assert_equal 'foo', @conn.type_cast(:foo, nil) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date, nil) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time, nil) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10, nil) + assert_equal 2.2, @conn.type_cast(2.2, nil) + end + + def test_type_cast_nil + assert_equal nil, @conn.type_cast(nil, nil) + end + + def test_type_cast_true + c = Column.new(nil, 1, 'int') + assert_equal 't', @conn.type_cast(true, nil) + assert_equal 1, @conn.type_cast(true, c) + end + + def test_type_cast_false + c = Column.new(nil, 1, 'int') + assert_equal 'f', @conn.type_cast(false, nil) + assert_equal 0, @conn.type_cast(false, c) + end + + def test_type_cast_string + assert_equal '10', @conn.type_cast('10', nil) + + c = Column.new(nil, 1, 'int') + assert_equal 10, @conn.type_cast('10', c) + + c = Column.new(nil, 1, 'float') + assert_equal 10.1, @conn.type_cast('10.1', c) + + c = Column.new(nil, 1, 'binary') + assert_equal '10.1', @conn.type_cast('10.1', c) + + c = Column.new(nil, 1, 'date') + assert_equal '10.1', @conn.type_cast('10.1', c) + end + + def test_type_cast_bigdecimal + bd = BigDecimal.new '10.0' + assert_equal bd.to_s('F'), @conn.type_cast(bd, nil) + end + + def test_type_cast_unknown + obj = Class.new.new + assert_equal YAML.dump(obj), @conn.type_cast(obj, nil) + end + + def test_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 + end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + end + end + 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 b8abdface4..6ff04e3eb3 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,12 +1,34 @@ +# encoding: utf-8 require "cases/helper" module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + class DualEncoding < ActiveRecord::Base + end + def setup @conn = Base.sqlite3_connection :database => ':memory:', :adapter => 'sqlite3', :timeout => 100 + @conn.execute <<-eosql + CREATE TABLE items ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + end + + def test_exec_insert + column = @conn.columns('items').find { |col| col.name == 'number' } + vals = [[column, 10]] + @conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals) + + result = @conn.exec_query( + 'select number from items where number = ?', 'SQL', vals) + + assert_equal 1, result.rows.length + assert_equal 10, result.rows.first.first end def test_primary_key_returns_nil_for_no_pk @@ -58,7 +80,7 @@ module ActiveRecord end def test_bind_value_substitute - bind_param = @conn.substitute_for('foo', []) + bind_param = @conn.substitute_at('foo', 0) assert_equal Arel.sql('?'), bind_param end @@ -102,6 +124,213 @@ module ActiveRecord assert_equal [[1, 'foo']], result.rows end + + def test_quote_binary_column_escapes_it + return unless "<3".respond_to?(:encode) + + DualEncoding.connection.execute(<<-eosql) + CREATE TABLE dual_encodings ( + id integer PRIMARY KEY AUTOINCREMENT, + name string, + data binary + ) + eosql + str = "\x80".force_encoding("ASCII-8BIT") + binary = DualEncoding.new :name => 'いただきます!', :data => str + binary.save! + assert_equal str, binary.data + end + + def test_execute + @conn.execute "INSERT INTO items (number) VALUES (10)" + records = @conn.execute "SELECT * FROM items" + assert_equal 1, records.length + + record = records.first + assert_equal 10, record['number'] + assert_equal 1, record['id'] + end + + def test_quote_string + assert_equal "''", @conn.quote_string("'") + end + + def test_insert_sql + 2.times do |i| + rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})" + assert_equal(i + 1, rv) + end + + records = @conn.execute "SELECT * FROM items" + assert_equal 2, records.length + end + + def test_insert_sql_logged + sql = "INSERT INTO items (number) VALUES (10)" + name = "foo" + + assert_logged([[sql, name, []]]) do + @conn.insert_sql sql, name + end + end + + def test_insert_id_value_returned + sql = "INSERT INTO items (number) VALUES (10)" + idval = 'vuvuzela' + id = @conn.insert_sql sql, nil, nil, idval + assert_equal idval, id + end + + def test_select_rows + 2.times do |i| + @conn.create "INSERT INTO items (number) VALUES (#{i})" + end + rows = @conn.select_rows 'select number, id from items' + assert_equal [[0, 1], [1, 2]], rows + end + + def test_select_rows_logged + sql = "select * from items" + name = "foo" + + assert_logged([[sql, name, []]]) do + @conn.select_rows sql, name + end + end + + def test_transaction + count_sql = 'select count(*) from items' + + @conn.begin_db_transaction + @conn.create "INSERT INTO items (number) VALUES (10)" + + assert_equal 1, @conn.select_rows(count_sql).first.first + @conn.rollback_db_transaction + assert_equal 0, @conn.select_rows(count_sql).first.first + end + + def test_tables + assert_equal %w{ items }, @conn.tables + + @conn.execute <<-eosql + CREATE TABLE people ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer + ) + eosql + assert_equal %w{ items people }.sort, @conn.tables.sort + end + + def test_tables_logs_name + name = "hello" + assert_logged [[name, []]] do + @conn.tables(name) + assert_not_nil @conn.logged.first.shift + end + end + + def test_columns + columns = @conn.columns('items').sort_by { |x| x.name } + assert_equal 2, columns.length + assert_equal %w{ id number }.sort, columns.map { |x| x.name } + assert_equal [nil, nil], columns.map { |x| x.default } + assert_equal [true, true], columns.map { |x| x.null } + end + + def test_columns_with_default + @conn.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer default 10 + ) + eosql + column = @conn.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert_equal 10, column.default + end + + def test_columns_with_not_null + @conn.execute <<-eosql + CREATE TABLE columns_with_default ( + id integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + column = @conn.columns('columns_with_default').find { |x| + x.name == 'number' + } + assert !column.null, "column should not be null" + end + + def test_indexes_logs + intercept_logs_on @conn + assert_difference('@conn.logged.length') do + @conn.indexes('items') + end + assert_match(/items/, @conn.logged.last.first) + end + + def test_no_indexes + assert_equal [], @conn.indexes('items') + end + + def test_index + @conn.add_index 'items', 'id', :unique => true, :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + + assert_equal 'items', index.table + assert index.unique, 'index is unique' + assert_equal ['id'], index.columns + end + + def test_non_unique_index + @conn.add_index 'items', 'id', :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + assert !index.unique, 'index is not unique' + end + + def test_compound_index + @conn.add_index 'items', %w{ id number }, :name => 'fun' + index = @conn.indexes('items').find { |idx| idx.name == 'fun' } + assert_equal %w{ id number }.sort, index.columns.sort + end + + def test_primary_key + assert_equal 'id', @conn.primary_key('items') + + @conn.execute <<-eosql + CREATE TABLE foos ( + internet integer PRIMARY KEY AUTOINCREMENT, + number integer not null + ) + eosql + assert_equal 'internet', @conn.primary_key('foos') + end + + def test_no_primary_key + @conn.execute 'CREATE TABLE failboat (number integer not null)' + assert_nil @conn.primary_key('failboat') + end + + private + + def assert_logged logs + intercept_logs_on @conn + yield + assert_equal logs, @conn.logged + end + + def intercept_logs_on ctx + @conn.extend(Module.new { + attr_accessor :logged + def log sql, name, binds = [] + @logged << [sql, name, binds] + yield + end + }) + @conn.logged = [] + end end end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 16d4877fe8..007f11b535 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -70,16 +70,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope car = cars(:honda) - original_scoped_methods = Bulb.scoped_methods + scoped_count = car.foo_bulbs.scoped.where_values.count - bulb = car.bulbs.build - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.build + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = car.bulbs.create - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.create + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = car.bulbs.create! - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = car.foo_bulbs.create! + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count end def test_no_sql_should_be_fired_if_association_already_loaded diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index c1dad5e246..f3c96ccbe6 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -165,16 +165,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_and_create_should_not_happen_within_scope pirate = pirates(:blackbeard) - original_scoped_methods = Bulb.scoped_methods.dup + scoped_count = pirate.association(:foo_bulb).scoped.where_values.count - bulb = pirate.build_bulb - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.build_foo_bulb + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = pirate.create_bulb - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.create_foo_bulb + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count - bulb = pirate.create_bulb! - assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize + bulb = pirate.create_foo_bulb! + assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count end def test_create_association diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 5a7b139030..49a1c117bc 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/tag' require 'models/tagging' require 'models/post' @@ -453,7 +454,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert saved_post.reload.tags(true).include?(new_tag) + assert new_tag.in?(saved_post.reload.tags(true)) new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.") @@ -466,7 +467,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert new_post.reload.tags(true).include?(saved_tag) + assert saved_tag.in?(new_post.reload.tags(true)) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -515,10 +516,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_collection_size_uses_counter_cache_if_it_exists - author = authors(:david) - author.stubs(:_read_attribute).with('comments_count').returns(100) - assert_equal 100, author.comments.size - assert !author.comments.loaded? + c = categories(:general) + c.categorizations_count = 100 + assert_equal 100, c.categorizations.size + assert !c.categorizations.loaded? end def test_adding_junk_to_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index d0a9028264..3641031d12 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' module ActiveRecord module AttributeMethods @@ -41,13 +42,13 @@ module ActiveRecord instance = @klass.new @klass.column_names.each do |name| - assert ! instance.methods.map(&:to_s).include?(name) + assert !name.in?(instance.methods.map(&:to_s)) end @klass.define_attribute_methods @klass.column_names.each do |name| - assert(instance.methods.map(&:to_s).include?(name), "#{name} is not defined") + assert name.in?(instance.methods.map(&:to_s)), "#{name} is not defined" end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 84f75cc803..5074ae50ab 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/minimalistic' require 'models/developer' require 'models/auto_id' @@ -9,6 +10,7 @@ require 'models/company' require 'models/category' require 'models/reply' require 'models/contact' +require 'models/keyboard' class AttributeMethodsTest < ActiveRecord::TestCase fixtures :topics, :developers, :companies, :computers @@ -76,6 +78,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_respond_to? topic = Topic.find(1) assert_respond_to topic, "title" + assert_respond_to topic, "_title" assert_respond_to topic, "title?" assert_respond_to topic, "title=" assert_respond_to topic, :title @@ -87,6 +90,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert !topic.respond_to?(:nothingness) end + def test_respond_to_with_custom_primary_key + keyboard = Keyboard.create + assert_not_nil keyboard.key_number + assert_equal keyboard.key_number, keyboard.id + assert keyboard.respond_to?('key_number') + assert keyboard.respond_to?('_key_number') + assert keyboard.respond_to?('id') + assert keyboard.respond_to?('_id') + end + # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object topic = Topic.allocate @@ -638,7 +651,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end def time_related_columns_on_topic - Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) } + Topic.columns.select { |c| c.type.in?([:time, :date, :datetime, :timestamp]) } end def serialized_columns_on_topic diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index aeb0b28bab..e57c5b3b87 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -172,7 +172,7 @@ class BasicsTest < ActiveRecord::TestCase with_active_record_default_timezone :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a @@ -186,7 +186,7 @@ class BasicsTest < ActiveRecord::TestCase Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a @@ -199,7 +199,7 @@ class BasicsTest < ActiveRecord::TestCase with_env_tz 'America/New_York' do time = Time.utc(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a @@ -212,7 +212,7 @@ class BasicsTest < ActiveRecord::TestCase Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) topic = Topic.create('written_on' => time) - saved_time = Topic.find(topic.id).written_on + saved_time = Topic.find(topic.id).reload.written_on assert_equal time, saved_time assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a @@ -1048,7 +1048,7 @@ class BasicsTest < ActiveRecord::TestCase topic = Topic.new(:content => myobj) assert topic.save Topic.serialize(:content, Hash) - assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content } + assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).reload.content } ensure Topic.serialize(:content) end @@ -1626,18 +1626,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_equal c1, c2 end - def test_default_scope_is_reset + def test_current_scope_is_reset Object.const_set :UnloadablePost, Class.new(ActiveRecord::Base) - UnloadablePost.table_name = 'posts' - UnloadablePost.class_eval do - default_scope order('posts.comments_count ASC') - end - UnloadablePost.scoped_methods # make Thread.current[:UnloadablePost_scoped_methods] not nil + UnloadablePost.send(:current_scope=, UnloadablePost.scoped) UnloadablePost.unloadable - assert_not_nil Thread.current[:UnloadablePost_scoped_methods] + assert_not_nil Thread.current[:UnloadablePost_current_scope] ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil Thread.current[:UnloadablePost_scoped_methods] + assert_nil Thread.current[:UnloadablePost_current_scope] ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 8545ba97cc..06c14cb108 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -1,3 +1,4 @@ +# encoding: utf-8 require "cases/helper" # Without using prepared statements, it makes no sense to test @@ -9,6 +10,24 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter) class BinaryTest < ActiveRecord::TestCase FIXTURES = %w(flowers.jpg example.log) + def test_mixed_encoding + str = "\x80" + str.force_encoding('ASCII-8BIT') if str.respond_to?(:force_encoding) + + binary = Binary.new :name => 'いただきます!', :data => str + binary.save! + binary.reload + assert_equal str, binary.data + + name = binary.name + + # Mysql adapter doesn't properly encode things, so we have to do it + if current_adapter?(:MysqlAdapter) + name.force_encoding('UTF-8') if name.respond_to?(:force_encoding) + end + assert_equal 'いただきます!', name + end + def test_load_save Binary.delete_all diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 19383bb06b..3652255c38 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -33,7 +33,7 @@ module ActiveRecord # FIXME: use skip with minitest return unless @connection.supports_statement_cache? - sub = @connection.substitute_for(@pk, []) + sub = @connection.substitute_at(@pk, 0) binds = [[@pk, 1]] sql = "select * from topics where id = #{sub}" diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index deaf5252db..b3a281d960 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require 'active_support/core_ext/object/inclusion' require 'models/default' require 'models/entrant' @@ -94,7 +95,7 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter) assert_equal 0, klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null # 0 in MySQL 4, nil in 5. - assert [0, nil].include?(klass.columns_hash['omit'].default) + assert klass.columns_hash['omit'].default.in?([0, nil]) assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 3c242667eb..655437318f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -247,9 +247,10 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.find(1, :select => "author_name") assert_raise(ActiveModel::MissingAttributeError) {topic.title} + assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name assert !topic.attribute_present?("title") - #assert !topic.respond_to?("title") + assert !topic.attribute_present?(:title) assert topic.attribute_present?("author_name") assert_respond_to topic, "author_name" end diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index 89f7b92d09..199e59657d 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -90,6 +90,13 @@ class IdentityMapTest < ActiveRecord::TestCase ) end + def test_queries_are_not_executed_when_finding_by_id + Post.find 1 + assert_no_queries do + Post.find 1 + end + end + ############################################################################## # Tests checking if IM is functioning properly on more advanced finds # # and associations # @@ -144,7 +151,7 @@ class IdentityMapTest < ActiveRecord::TestCase s = Subscriber.find('swistak') - assert_equal({'name' => ["Raczkowski Marcin", "Swistak Sreberkowiec"]}, swistak.changes) + assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) assert_equal("Swistak Sreberkowiec", swistak.name) end @@ -159,8 +166,8 @@ class IdentityMapTest < ActiveRecord::TestCase s = Subscriber.find('swistak') assert_equal("Swistak Sreberkowiec", swistak.name) - assert_equal({}, swistak.changes) - assert !swistak.name_changed? + assert_equal({"name"=>["Marcin Raczkowski", "Swistak Sreberkowiec"]}, swistak.changes) + assert swistak.name_changed? end def test_has_many_associations @@ -381,6 +388,15 @@ class IdentityMapTest < ActiveRecord::TestCase assert_not_nil post.title end + def test_log + log = StringIO.new + ActiveRecord::Base.logger = Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG + Post.find 1 + Post.find 1 + assert_match(/Post with ID = 1 loaded from Identity Map/, log.string) + end + # Currently AR is not allowing changing primary key (see Persistence#update) # So we ignore it. If this changes, this test needs to be uncommented. # def test_updating_of_pkey diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 8ebde933b4..5f55299065 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -8,6 +8,8 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger + @using_identity_map = ActiveRecord::IdentityMap.enabled? + ActiveRecord::IdentityMap.enabled = false super ActiveRecord::LogSubscriber.attach_to(:active_record) end @@ -16,6 +18,7 @@ class LogSubscriberTest < ActiveRecord::TestCase super ActiveRecord::LogSubscriber.log_subscribers.pop ActiveRecord::Base.logger = @old_logger + ActiveRecord::IdentityMap.enabled = @using_identity_map end def set_logger(logger) diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index 7e8383da9e..7f0f007a70 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -249,22 +249,21 @@ class MethodScopingTest < ActiveRecord::TestCase end def test_scoped_with_duck_typing - scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] }) + scoping = Struct.new(:current_scope).new(:find => { :conditions => ["name = ?", 'David'] }) Developer.send(:with_scope, scoping) do assert_equal %w(David), Developer.find(:all).map { |d| d.name } end end def test_ensure_that_method_scoping_is_correctly_restored - scoped_methods = Developer.instance_eval('current_scoped_methods') - begin Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do raise "an exception" end rescue end - assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + + assert !Developer.scoped.where_values.include?("name = 'Jamis'") end end @@ -509,14 +508,15 @@ class NestedScopingTest < ActiveRecord::TestCase def test_ensure_that_method_scoping_is_correctly_restored Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do - scoped_methods = Developer.instance_eval('current_scoped_methods') begin Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do raise "an exception" end rescue end - assert_equal scoped_methods, Developer.instance_eval('current_scoped_methods') + + assert Developer.scoped.where_values.include?("name = 'David'") + assert !Developer.scoped.where_values.include?("name = 'Maiha'") end end diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index 9b20ea08de..2880fdc651 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -58,17 +58,6 @@ class NamedScopeTest < ActiveRecord::TestCase assert Topic.approved.respond_to?(:tables_in_string, true) end - def test_subclasses_inherit_scopes - assert Topic.scopes.include?(:base) - - assert Reply.scopes.include?(:base) - assert_equal Reply.find(:all), Reply.base - end - - def test_classes_dont_inherit_subclasses_scopes - assert !ActiveRecord::Base.scopes.include?(:base) - end - def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified assert !Topic.find(:all, :conditions => {:approved => true}).empty? @@ -440,26 +429,31 @@ class NamedScopeTest < ActiveRecord::TestCase end end + # Note: these next two are kinda odd because they are essentially just testing that the + # query cache works as it should, but they are here for legacy reasons as they was previously + # a separate cache on association proxies, and these show that that is not necessary. def test_scopes_are_cached_on_associations post = posts(:welcome) - assert_equal post.comments.containing_the_letter_e.object_id, post.comments.containing_the_letter_e.object_id - - post.comments.containing_the_letter_e.all # force load - assert_no_queries { post.comments.containing_the_letter_e.all } + Post.cache do + assert_queries(1) { post.comments.containing_the_letter_e.all } + assert_no_queries { post.comments.containing_the_letter_e.all } + end end def test_scopes_with_arguments_are_cached_on_associations post = posts(:welcome) - one = post.comments.limit_by(1).all - assert_equal 1, one.size + Post.cache do + one = assert_queries(1) { post.comments.limit_by(1).all } + assert_equal 1, one.size - two = post.comments.limit_by(2).all - assert_equal 2, two.size + two = assert_queries(1) { post.comments.limit_by(2).all } + assert_equal 2, two.size - assert_no_queries { post.comments.limit_by(1).all } - assert_no_queries { post.comments.limit_by(2).all } + assert_no_queries { post.comments.limit_by(1).all } + assert_no_queries { post.comments.limit_by(2).all } + end end def test_scopes_are_reset_on_association_reload @@ -477,6 +471,12 @@ class NamedScopeTest < ActiveRecord::TestCase require "models/without_table" end end + + def test_scopes_with_callables_are_deprecated + assert_deprecated do + Post.scope :WE_SO_EXCITED, lambda { |partyingpartyingpartying, yeah| fun!.fun!.fun! } + end + end end class DynamicScopeMatchTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index c57ab7ed28..6568eb1d18 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -131,6 +131,14 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal 'photography', interest.reload.topic end + def test_destroy_works_independent_of_reject_if + Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }, :allow_destroy => true + man = Man.create(:name => "Jon") + interest = man.interests.create(:topic => 'the ladies') + man.update_attributes({:interests_attributes => { :_destroy => "1", :id => interest.id } }) + assert man.reload.interests.empty? + end + def test_has_many_association_updating_a_single_record Man.accepts_nested_attributes_for(:interests) man = Man.create(:name => 'John') diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 53aefc7b58..287f7e255b 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -13,7 +13,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_find_queries - assert_queries(2) { Task.find(1); Task.find(1) } + assert_queries(ActiveRecord::IdentityMap.enabled? ? 1 : 2) { Task.find(1); Task.find(1) } end def test_find_queries_with_cache diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index b87fb51d97..80ee74e41e 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -154,15 +154,16 @@ module ActiveRecord end def test_crazy_object - crazy = Class.new { def to_yaml; 'lol' end }.new - assert_equal "'lol'", @quoter.quote(crazy, nil) - assert_equal "'lol'", @quoter.quote(crazy, Object.new) + crazy = Class.new.new + expected = "'#{YAML.dump(crazy)}'" + assert_equal expected, @quoter.quote(crazy, nil) + assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string - crazy = Class.new { def to_yaml; 'lo\l' end }.new - assert_equal "'lo\\\\l'", @quoter.quote(crazy, nil) - assert_equal "'lo\\\\l'", @quoter.quote(crazy, Object.new) + crazy = Class.new { def initialize; @lol = 'lo\l' end }.new + assert_match "lo\\\\l", @quoter.quote(crazy, nil) + assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index 30a783d5a2..5079aec9ba 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -132,8 +132,6 @@ class RelationScopingTest < ActiveRecord::TestCase end def test_ensure_that_method_scoping_is_correctly_restored - scoped_methods = Developer.send(:current_scoped_methods) - begin Developer.where("name = 'Jamis'").scoping do raise "an exception" @@ -141,7 +139,7 @@ class RelationScopingTest < ActiveRecord::TestCase rescue end - assert_equal scoped_methods, Developer.send(:current_scoped_methods) + assert !Developer.scoped.where_values.include?("name = 'Jamis'") end end @@ -310,72 +308,178 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received end - def test_default_scope_with_lambda - expected = Post.find_all_by_author_id(2) - PostForAuthor.selected_author = 2 - received = PostForAuthor.all - assert_equal expected, received - expected = Post.find_all_by_author_id(1) - PostForAuthor.selected_author = 1 - received = PostForAuthor.all + def test_default_scope_is_unscoped_on_find + assert_equal 1, DeveloperCalledDavid.count + assert_equal 11, DeveloperCalledDavid.unscoped.count + end + + def test_default_scope_is_unscoped_on_create + assert_nil DeveloperCalledJamis.unscoped.create!.name + end + + def test_default_scope_with_conditions_string + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal nil, DeveloperCalledDavid.create!.name + end + + def test_default_scope_with_conditions_hash + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal 'Jamis', DeveloperCalledJamis.create!.name + end + + def test_default_scoping_with_threads + 2.times do + Thread.new { assert DeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join + end + end + + def test_default_scope_with_inheritance + wheres = InheritedPoorDeveloperCalledJamis.scoped.where_values_hash + assert_equal "Jamis", wheres[:name] + assert_equal 50000, wheres[:salary] + end + + def test_method_scope + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } assert_equal expected, received end - def test_default_scope_with_thing_that_responds_to_call - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'posts' + def test_nested_scope + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end + assert_equal expected, received + end - klass.class_eval do - default_scope Class.new(Struct.new(:klass)) { - def call - klass.where(:author_id => 2) - end - }.new(self) + def test_scope_overwrites_default + expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + assert_equal expected, received + end + + def test_reorder_overrides_default_scope_order + expected = Developer.order('name DESC').collect { |dev| dev.name } + received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + assert_equal expected, received + end + + def test_nested_exclusive_scope + expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do + DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end + assert_equal expected, received + end + + def test_order_in_default_scope_should_prevail + expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } + received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + assert_equal expected, received + end + + def test_create_attribute_overwrites_default_scoping + assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name + assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary + end + + def test_create_attribute_overwrites_default_values + assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary + assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary + end + + def test_default_scope_attribute + jamis = PoorDeveloperCalledJamis.new(:name => 'David') + assert_equal 50000, jamis.salary + end - records = klass.all - assert_equal 3, records.length - assert_equal 2, records.first.author_id + def test_where_attribute + aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end + + def test_where_attribute_merge + aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') + assert_equal 'Aaron', aaron.name + end + + def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit + posts_limit_offset = Post.limit(3).offset(2) + posts_offset_limit = Post.offset(2).limit(3) + assert_equal posts_limit_offset, posts_offset_limit + end + + def test_create_with_merge + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( + PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + + aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). + create_with(:name => 'Aaron').new + assert_equal 20, aaron.salary + assert_equal 'Aaron', aaron.name + end + + def test_create_with_reset + jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new + assert_equal 'Jamis', jamis.name + end + + def test_unscoped_with_named_scope_should_not_have_default_scope + assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor + + assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) + assert_equal 10, DeveloperCalledJamis.unscoped.poor.length + end +end + +class DeprecatedDefaultScopingTest < ActiveRecord::TestCase + fixtures :developers, :posts + + def test_default_scope + expected = Developer.find(:all, :order => 'salary DESC').collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + assert_equal expected, received end def test_default_scope_is_unscoped_on_find - assert_equal 1, DeveloperCalledDavid.count - assert_equal 11, DeveloperCalledDavid.unscoped.count + assert_equal 1, DeprecatedDeveloperCalledDavid.count + assert_equal 11, DeprecatedDeveloperCalledDavid.unscoped.count end def test_default_scope_is_unscoped_on_create - assert_nil DeveloperCalledJamis.unscoped.create!.name + assert_nil DeprecatedDeveloperCalledJamis.unscoped.create!.name end def test_default_scope_with_conditions_string - assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeveloperCalledDavid.find(:all).map(&:id).sort - assert_equal nil, DeveloperCalledDavid.create!.name + assert_equal Developer.find_all_by_name('David').map(&:id).sort, DeprecatedDeveloperCalledDavid.find(:all).map(&:id).sort + assert_equal nil, DeprecatedDeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash - assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeveloperCalledJamis.find(:all).map(&:id).sort - assert_equal 'Jamis', DeveloperCalledJamis.create!.name + assert_equal Developer.find_all_by_name('Jamis').map(&:id).sort, DeprecatedDeveloperCalledJamis.find(:all).map(&:id).sort + assert_equal 'Jamis', DeprecatedDeveloperCalledJamis.create!.name end def test_default_scoping_with_threads 2.times do - Thread.new { assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values }.join + Thread.new { assert DeprecatedDeveloperOrderedBySalary.scoped.to_sql.include?('salary DESC') }.join end end def test_default_scoping_with_inheritance # Inherit a class having a default scope and define a new default scope - klass = Class.new(DeveloperOrderedBySalary) - klass.send :default_scope, :limit => 1 + klass = Class.new(DeprecatedDeveloperOrderedBySalary) + ActiveSupport::Deprecation.silence { klass.send :default_scope, :limit => 1 } # Scopes added on children should append to parent scope - assert_equal 1, klass.scoped.limit_value - assert_equal ['salary DESC'], klass.scoped.order_values + assert_equal [developers(:jamis).id], klass.all.map(&:id) # Parent should still have the original scope - assert_nil DeveloperOrderedBySalary.scoped.limit_value - assert_equal ['salary DESC'], DeveloperOrderedBySalary.scoped.order_values + assert_equal Developer.order('salary DESC').map(&:id), DeprecatedDeveloperOrderedBySalary.all.map(&:id) end def test_default_scope_called_twice_merges_conditions @@ -385,8 +489,10 @@ class DefaultScopingTest < ActiveRecord::TestCase Developer.create!(:name => "Brian", :salary => 100000) klass = Class.new(Developer) - klass.__send__ :default_scope, :conditions => { :name => "David" } - klass.__send__ :default_scope, :conditions => { :salary => 100000 } + ActiveSupport::Deprecation.silence do + klass.__send__ :default_scope, :conditions => { :name => "David" } + klass.__send__ :default_scope, :conditions => { :salary => 100000 } + end assert_equal 1, klass.count assert_equal "David", klass.first.name assert_equal 100000, klass.first.salary @@ -399,9 +505,11 @@ class DefaultScopingTest < ActiveRecord::TestCase Developer.create!(:name => "Brian", :salary => 100000) klass = Class.new(Developer) - klass.class_eval do - default_scope where("name = 'David'") - default_scope where("salary = 100000") + ActiveSupport::Deprecation.silence do + klass.class_eval do + default_scope where("name = 'David'") + default_scope where("salary = 100000") + end end assert_equal 1, klass.count @@ -411,96 +519,90 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_method_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.all_ordered_by_name.collect { |dev| dev.salary } assert_equal expected, received end def test_nested_scope expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do + DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end assert_equal expected, received end def test_scope_overwrites_default expected = Developer.find(:all, :order => 'salary DESC, name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } + received = DeprecatedDeveloperOrderedBySalary.by_name.find(:all).collect { |dev| dev.name } assert_equal expected, received end def test_reorder_overrides_default_scope_order expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + received = DeprecatedDeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } assert_equal expected, received end def test_nested_exclusive_scope expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do - DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do + DeprecatedDeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary } end assert_equal expected, received end def test_order_in_default_scope_should_prevail expected = Developer.find(:all, :order => 'salary desc').collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } + received = DeprecatedDeveloperOrderedBySalary.find(:all, :order => 'salary').collect { |dev| dev.salary } assert_equal expected, received end def test_default_scope_using_relation - posts = PostWithComment.scoped - assert_equal 2, posts.count + posts = DeprecatedPostWithComment.scoped + assert_equal 2, posts.to_a.length assert_equal posts(:thinking), posts.first end def test_create_attribute_overwrites_default_scoping - assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name - assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary + assert_equal 'David', DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').name + assert_equal 200000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary end def test_create_attribute_overwrites_default_values - assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary - assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary + assert_equal nil, DeprecatedPoorDeveloperCalledJamis.create!(:salary => nil).salary + assert_equal 50000, DeprecatedPoorDeveloperCalledJamis.create!(:name => 'David').salary end def test_default_scope_attribute - jamis = PoorDeveloperCalledJamis.new(:name => 'David') + jamis = DeprecatedPoorDeveloperCalledJamis.new(:name => 'David') assert_equal 50000, jamis.salary end def test_where_attribute - aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') + aaron = DeprecatedPoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron') assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name end def test_where_attribute_merge - aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') + aaron = DeprecatedPoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron') assert_equal 'Aaron', aaron.name end - def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit - posts_limit_offset = Post.limit(3).offset(2) - posts_offset_limit = Post.offset(2).limit(3) - assert_equal posts_limit_offset, posts_offset_limit - end - def test_create_with_merge - aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( - PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new + aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge( + DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name - aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). + aaron = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20). create_with(:name => 'Aaron').new assert_equal 20, aaron.salary assert_equal 'Aaron', aaron.name end def test_create_with_reset - jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new + jamis = DeprecatedPoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new assert_equal 'Jamis', jamis.name end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 7bdbd773b6..6874bd18f8 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,7 +20,7 @@ module ActiveRecord end def test_single_values - assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from].map(&:to_s).sort, + assert_equal [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder].map(&:to_s).sort, Relation::SINGLE_VALUE_METHODS.map(&:to_s).sort end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 7178bb0d00..89ee5416bf 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,14 +1,15 @@ class Bulb < ActiveRecord::Base - - default_scope :conditions => {:name => 'defaulty' } + def self.default_scope + where :name => 'defaulty' + end belongs_to :car - attr_reader :scoped_methods_after_initialize + attr_reader :scope_after_initialize - after_initialize :record_scoped_methods_after_initialize - def record_scoped_methods_after_initialize - @scoped_methods_after_initialize = self.class.scoped_methods.dup + after_initialize :record_scope_after_initialize + def record_scope_after_initialize + @scope_after_initialize = self.class.scoped end end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index e7db3d3423..a978debb58 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -1,6 +1,7 @@ class Car < ActiveRecord::Base has_many :bulbs + has_many :foo_bulbs, :class_name => "Bulb", :conditions => { :name => 'foo' } has_many :tyres has_many :engines has_many :wheels, :as => :wheelable @@ -14,9 +15,13 @@ class Car < ActiveRecord::Base end class CoolCar < Car - default_scope :order => 'name desc' + def self.default_scope + order 'name desc' + end end class FastCar < Car - default_scope order('name desc') + def self.default_scope + order 'name desc' + end end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 09489b8ea4..39441e8610 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -13,7 +13,9 @@ end class SpecialCategorization < ActiveRecord::Base self.table_name = 'categorizations' - default_scope where(:special => true) + def self.default_scope + where(:special => true) + end belongs_to :author belongs_to :category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 2a4c37089a..3bd7db7834 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,5 +1,8 @@ class Comment < ActiveRecord::Base - scope :limit_by, lambda {|l| limit(l) } + def self.limit_by(l) + limit(l) + end + scope :containing_the_letter_e, :conditions => "comments.body LIKE '%e%'" scope :not_again, where("comments.body NOT LIKE '%again%'") scope :for_first_post, :conditions => { :post_id => 1 } diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 32d060cf09..10385ba899 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -86,7 +86,11 @@ end class DeveloperOrderedBySalary < ActiveRecord::Base self.table_name = 'developers' - default_scope :order => 'salary DESC' + + def self.default_scope + order('salary DESC') + end + scope :by_name, order('name DESC') def self.all_ordered_by_name @@ -98,15 +102,74 @@ end class DeveloperCalledDavid < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => "name = 'David'" + + def self.default_scope + where "name = 'David'" + end end class DeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis' } + + def self.default_scope + where :name => 'Jamis' + end + + scope :poor, where('salary < 150000') +end + +class AbstractDeveloperCalledJamis < ActiveRecord::Base + self.abstract_class = true + + def self.default_scope + where :name => 'Jamis' + end end class PoorDeveloperCalledJamis < ActiveRecord::Base self.table_name = 'developers' - default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + + def self.default_scope + where :name => 'Jamis', :salary => 50000 + end +end + +class InheritedPoorDeveloperCalledJamis < DeveloperCalledJamis + self.table_name = 'developers' + + def self.default_scope + super.where :salary => 50000 + end +end + +ActiveSupport::Deprecation.silence do + class DeprecatedDeveloperOrderedBySalary < ActiveRecord::Base + self.table_name = 'developers' + default_scope :order => 'salary DESC' + + def self.by_name + order('name DESC') + end + + def self.all_ordered_by_name + with_scope(:find => { :order => 'name DESC' }) do + find(:all) + end + end + end + + class DeprecatedDeveloperCalledDavid < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => "name = 'David'" + end + + class DeprecatedDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => { :name => 'Jamis' } + end + + class DeprecatedPoorDeveloperCalledJamis < ActiveRecord::Base + self.table_name = 'developers' + default_scope :conditions => { :name => 'Jamis', :salary => 50000 } + end end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 0d3f62bb33..5e0f5323e6 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -34,7 +34,7 @@ class Pirate < ActiveRecord::Base :after_remove => proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"} has_many :birds_with_reject_all_blank, :class_name => "Bird" - has_one :bulb, :foreign_key => :car_id + has_one :foo_bulb, :foreign_key => :car_id, :class_name => "Bulb", :conditions => { :name => 'foo' } accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 82894a3d57..a91c10276b 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -7,12 +7,14 @@ class Post < ActiveRecord::Base scope :containing_the_letter_a, where("body LIKE '%a%'") scope :ranked_by_comments, order("comments_count DESC") - scope :limit_by, lambda {|l| limit(l) } - scope :with_authors_at_address, lambda { |address| { - :conditions => [ 'authors.author_address_id = ?', address.id ], - :joins => 'JOIN authors ON authors.id = posts.author_id' - } - } + + def self.limit_by(l) + limit(l) + end + + def self.with_authors_at_address(address) + where('authors.author_address_id = ?', address.id).joins('JOIN authors ON authors.id = posts.author_id') + end belongs_to :author do def greeting @@ -27,9 +29,10 @@ class Post < ActiveRecord::Base scope :with_special_comments, :joins => :comments, :conditions => {:comments => {:type => 'SpecialComment'} } scope :with_very_special_comments, joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) - scope :with_post, lambda {|post_id| - { :joins => :comments, :conditions => {:comments => {:post_id => post_id} } } - } + + def self.with_post(post_id) + joins(:comments).where(:comments => { :post_id => post_id }) + end has_many :comments do def find_most_recent @@ -142,20 +145,25 @@ class SubStiPost < StiPost self.table_name = Post.table_name end -class PostWithComment < ActiveRecord::Base - self.table_name = 'posts' - default_scope where("posts.comments_count > 0").order("posts.comments_count ASC") +ActiveSupport::Deprecation.silence do + class DeprecatedPostWithComment < ActiveRecord::Base + self.table_name = 'posts' + default_scope where("posts.comments_count > 0").order("posts.comments_count ASC") + end end class PostForAuthor < ActiveRecord::Base self.table_name = 'posts' cattr_accessor :selected_author - default_scope lambda { where(:author_id => PostForAuthor.selected_author) } end class FirstPost < ActiveRecord::Base self.table_name = 'posts' - default_scope where(:id => 1) + + def self.default_scope + where(:id => 1) + end + has_many :comments, :foreign_key => :post_id has_one :comment, :foreign_key => :post_id end diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb index e33a0f2acc..76c0a1a32e 100644 --- a/activerecord/test/models/reference.rb +++ b/activerecord/test/models/reference.rb @@ -18,6 +18,9 @@ class Reference < ActiveRecord::Base end class BadReference < ActiveRecord::Base - self.table_name ='references' - default_scope :conditions => {:favourite => false } + self.table_name = 'references' + + def self.default_scope + where :favourite => false + end end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 6440dbe8ab..60e750e6c4 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -1,10 +1,20 @@ class Topic < ActiveRecord::Base scope :base - scope :written_before, lambda { |time| - if time - { :conditions => ['written_on < ?', time] } - end - } + + ActiveSupport::Deprecation.silence do + scope :written_before, lambda { |time| + if time + { :conditions => ['written_on < ?', time] } + end + } + + scope :with_object, Class.new(Struct.new(:klass)) { + def call + klass.where(:approved => true) + end + }.new(self) + end + scope :approved, :conditions => {:approved => true} scope :rejected, :conditions => {:approved => false} @@ -19,12 +29,6 @@ class Topic < ActiveRecord::Base end end - scope :with_object, Class.new(Struct.new(:klass)) { - def call - klass.where(:approved => true) - end - }.new(self) - module NamedExtension def two 2 diff --git a/activerecord/test/models/without_table.rb b/activerecord/test/models/without_table.rb index 87f80911e1..1a63d6ceb6 100644 --- a/activerecord/test/models/without_table.rb +++ b/activerecord/test/models/without_table.rb @@ -1,3 +1,5 @@ class WithoutTable < ActiveRecord::Base - default_scope where(:published => true) -end
\ No newline at end of file + def self.default_scope + where(:published => true) + end +end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 362475de36..ceadb05644 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -67,6 +67,7 @@ ActiveRecord::Schema.define do end create_table :binaries, :force => true do |t| + t.string :name t.binary :data end |