diff options
Diffstat (limited to 'activerecord/lib')
33 files changed, 653 insertions, 301 deletions
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 |