mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Split out most of the AR::Base code into separate modules 🍰
This commit is contained in:
parent
8854bf29a3
commit
ceb33f8493
19 changed files with 1794 additions and 1671 deletions
|
@ -39,6 +39,7 @@ module ActiveRecord
|
|||
autoload :Aggregations
|
||||
autoload :Associations
|
||||
autoload :AttributeMethods
|
||||
autoload :AttributeAssignment
|
||||
autoload :AutosaveAssociation
|
||||
|
||||
autoload :Relation
|
||||
|
@ -57,27 +58,37 @@ module ActiveRecord
|
|||
autoload :Base
|
||||
autoload :Callbacks
|
||||
autoload :CounterCache
|
||||
autoload :DefaultScope
|
||||
autoload :DynamicMatchers
|
||||
autoload :DynamicFinderMatch
|
||||
autoload :DynamicScopeMatch
|
||||
autoload :Explain
|
||||
autoload :IdentityMap
|
||||
autoload :Inheritance
|
||||
autoload :Integration
|
||||
autoload :Migration
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :ModelSchema
|
||||
autoload :NamedScope
|
||||
autoload :NestedAttributes
|
||||
autoload :Observer
|
||||
autoload :Persistence
|
||||
autoload :QueryCache
|
||||
autoload :Querying
|
||||
autoload :ReadonlyAttributes
|
||||
autoload :Reflection
|
||||
autoload :Result
|
||||
autoload :Sanitization
|
||||
autoload :Schema
|
||||
autoload :SchemaDumper
|
||||
autoload :Scoping
|
||||
autoload :Serialization
|
||||
autoload :Store
|
||||
autoload :SessionStore
|
||||
autoload :Store
|
||||
autoload :Timestamp
|
||||
autoload :Transactions
|
||||
autoload :Translation
|
||||
autoload :Validations
|
||||
autoload :IdentityMap
|
||||
autoload :Explain
|
||||
end
|
||||
|
||||
module Coders
|
||||
|
|
221
activerecord/lib/active_record/attribute_assignment.rb
Normal file
221
activerecord/lib/active_record/attribute_assignment.rb
Normal file
|
@ -0,0 +1,221 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module AttributeAssignment
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::MassAssignmentSecurity
|
||||
|
||||
module ClassMethods
|
||||
private
|
||||
|
||||
# The primary key and inheritance column can never be set by mass-assignment for security reasons.
|
||||
def attributes_protected_by_default
|
||||
default = [ primary_key, inheritance_column ]
|
||||
default << 'id' unless primary_key.eql? 'id'
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
# Allows you to set all the attributes at once by passing in a hash with keys
|
||||
# matching the attribute names (which again matches the column names).
|
||||
#
|
||||
# If any attributes are protected by either +attr_protected+ or
|
||||
# +attr_accessible+ then only settable attributes will be assigned.
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# attr_protected :is_admin
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
# user.attributes = { :username => 'Phusion', :is_admin => true }
|
||||
# user.username # => "Phusion"
|
||||
# user.is_admin? # => false
|
||||
def attributes=(new_attributes)
|
||||
return unless new_attributes.is_a?(Hash)
|
||||
|
||||
assign_attributes(new_attributes)
|
||||
end
|
||||
|
||||
# Allows you to set all the attributes for a particular mass-assignment
|
||||
# security role by passing in a hash of attributes with keys matching
|
||||
# the attribute names (which again matches the column names) and the role
|
||||
# name using the :as option.
|
||||
#
|
||||
# To bypass mass-assignment security you can use the :without_protection => true
|
||||
# option.
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# attr_accessible :name
|
||||
# attr_accessible :name, :is_admin, :as => :admin
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
# user.assign_attributes({ :name => 'Josh', :is_admin => true })
|
||||
# user.name # => "Josh"
|
||||
# user.is_admin? # => false
|
||||
#
|
||||
# user = User.new
|
||||
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :as => :admin)
|
||||
# user.name # => "Josh"
|
||||
# user.is_admin? # => true
|
||||
#
|
||||
# user = User.new
|
||||
# user.assign_attributes({ :name => 'Josh', :is_admin => true }, :without_protection => true)
|
||||
# user.name # => "Josh"
|
||||
# user.is_admin? # => true
|
||||
def assign_attributes(new_attributes, options = {})
|
||||
return unless new_attributes
|
||||
|
||||
attributes = new_attributes.stringify_keys
|
||||
multi_parameter_attributes = []
|
||||
nested_parameter_attributes = []
|
||||
@mass_assignment_options = options
|
||||
|
||||
unless options[:without_protection]
|
||||
attributes = sanitize_for_mass_assignment(attributes, mass_assignment_role)
|
||||
end
|
||||
|
||||
attributes.each do |k, v|
|
||||
if k.include?("(")
|
||||
multi_parameter_attributes << [ k, v ]
|
||||
elsif respond_to?("#{k}=")
|
||||
if v.is_a?(Hash)
|
||||
nested_parameter_attributes << [ k, v ]
|
||||
else
|
||||
send("#{k}=", v)
|
||||
end
|
||||
else
|
||||
raise(UnknownAttributeError, "unknown attribute: #{k}")
|
||||
end
|
||||
end
|
||||
|
||||
# assign any deferred nested attributes after the base attributes have been set
|
||||
nested_parameter_attributes.each do |k,v|
|
||||
send("#{k}=", v)
|
||||
end
|
||||
|
||||
@mass_assignment_options = nil
|
||||
assign_multiparameter_attributes(multi_parameter_attributes)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def mass_assignment_options
|
||||
@mass_assignment_options ||= {}
|
||||
end
|
||||
|
||||
def mass_assignment_role
|
||||
mass_assignment_options[:as] || :default
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done
|
||||
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
|
||||
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
|
||||
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
|
||||
# parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
|
||||
# f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
|
||||
# attribute will be set to nil.
|
||||
def assign_multiparameter_attributes(pairs)
|
||||
execute_callstack_for_multiparameter_attributes(
|
||||
extract_callstack_for_multiparameter_attributes(pairs)
|
||||
)
|
||||
end
|
||||
|
||||
def instantiate_time_object(name, values)
|
||||
if self.class.send(:create_time_zone_conversion_attribute?, name, column_for_attribute(name))
|
||||
Time.zone.local(*values)
|
||||
else
|
||||
Time.time_with_datetime_fallback(self.class.default_timezone, *values)
|
||||
end
|
||||
end
|
||||
|
||||
def execute_callstack_for_multiparameter_attributes(callstack)
|
||||
errors = []
|
||||
callstack.each do |name, values_with_empty_parameters|
|
||||
begin
|
||||
send(name + "=", read_value_from_parameter(name, values_with_empty_parameters))
|
||||
rescue => ex
|
||||
errors << AttributeAssignmentError.new("error on assignment #{values_with_empty_parameters.values.inspect} to #{name}", ex, name)
|
||||
end
|
||||
end
|
||||
unless errors.empty?
|
||||
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
|
||||
end
|
||||
end
|
||||
|
||||
def read_value_from_parameter(name, values_hash_from_param)
|
||||
klass = (self.class.reflect_on_aggregation(name.to_sym) || column_for_attribute(name)).klass
|
||||
if values_hash_from_param.values.all?{|v|v.nil?}
|
||||
nil
|
||||
elsif klass == Time
|
||||
read_time_parameter_value(name, values_hash_from_param)
|
||||
elsif klass == Date
|
||||
read_date_parameter_value(name, values_hash_from_param)
|
||||
else
|
||||
read_other_parameter_value(klass, name, values_hash_from_param)
|
||||
end
|
||||
end
|
||||
|
||||
def read_time_parameter_value(name, values_hash_from_param)
|
||||
# If Date bits were not provided, error
|
||||
raise "Missing Parameter" if [1,2,3].any?{|position| !values_hash_from_param.has_key?(position)}
|
||||
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param, 6)
|
||||
# If Date bits were provided but blank, then return nil
|
||||
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
|
||||
|
||||
set_values = (1..max_position).collect{|position| values_hash_from_param[position] }
|
||||
# If Time bits are not there, then default to 0
|
||||
(3..5).each {|i| set_values[i] = set_values[i].blank? ? 0 : set_values[i]}
|
||||
instantiate_time_object(name, set_values)
|
||||
end
|
||||
|
||||
def read_date_parameter_value(name, values_hash_from_param)
|
||||
return nil if (1..3).any? {|position| values_hash_from_param[position].blank?}
|
||||
set_values = [values_hash_from_param[1], values_hash_from_param[2], values_hash_from_param[3]]
|
||||
begin
|
||||
Date.new(*set_values)
|
||||
rescue ArgumentError # if Date.new raises an exception on an invalid date
|
||||
instantiate_time_object(name, set_values).to_date # we instantiate Time object and convert it back to a date thus using Time's logic in handling invalid dates
|
||||
end
|
||||
end
|
||||
|
||||
def read_other_parameter_value(klass, name, values_hash_from_param)
|
||||
max_position = extract_max_param_for_multiparameter_attributes(values_hash_from_param)
|
||||
values = (1..max_position).collect do |position|
|
||||
raise "Missing Parameter" if !values_hash_from_param.has_key?(position)
|
||||
values_hash_from_param[position]
|
||||
end
|
||||
klass.new(*values)
|
||||
end
|
||||
|
||||
def extract_max_param_for_multiparameter_attributes(values_hash_from_param, upper_cap = 100)
|
||||
[values_hash_from_param.keys.max,upper_cap].min
|
||||
end
|
||||
|
||||
def extract_callstack_for_multiparameter_attributes(pairs)
|
||||
attributes = { }
|
||||
|
||||
pairs.each do |pair|
|
||||
multiparameter_name, value = pair
|
||||
attribute_name = multiparameter_name.split("(").first
|
||||
attributes[attribute_name] = {} unless attributes.include?(attribute_name)
|
||||
|
||||
parameter_value = value.empty? ? nil : type_cast_attribute_value(multiparameter_name, value)
|
||||
attributes[attribute_name][find_parameter_position(multiparameter_name)] ||= parameter_value
|
||||
end
|
||||
|
||||
attributes
|
||||
end
|
||||
|
||||
def type_cast_attribute_value(multiparameter_name, value)
|
||||
multiparameter_name =~ /\([0-9]*([if])\)/ ? value.send("to_" + $1) : value
|
||||
end
|
||||
|
||||
def find_parameter_position(multiparameter_name)
|
||||
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -75,6 +75,21 @@ module ActiveRecord
|
|||
!superclass.method_defined?(method_name) &&
|
||||
!superclass.private_method_defined?(method_name)
|
||||
end
|
||||
|
||||
def attribute_method?(attribute)
|
||||
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
|
||||
end
|
||||
|
||||
# Returns an array of column names as strings if it's not
|
||||
# an abstract class and table exists.
|
||||
# Otherwise it returns an empty array.
|
||||
def attribute_names
|
||||
@attribute_names ||= if !abstract_class? && table_exists?
|
||||
column_names
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If we haven't generated any methods yet, generate them, then
|
||||
|
@ -112,9 +127,105 @@ module ActiveRecord
|
|||
super
|
||||
end
|
||||
|
||||
protected
|
||||
def attribute_method?(attr_name)
|
||||
attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
|
||||
# Returns true if the given attribute is in the attributes hash
|
||||
def has_attribute?(attr_name)
|
||||
@attributes.has_key?(attr_name.to_s)
|
||||
end
|
||||
|
||||
# Returns an array of names for the attributes available on this object.
|
||||
def attribute_names
|
||||
@attributes.keys
|
||||
end
|
||||
|
||||
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
|
||||
def attributes
|
||||
Hash[@attributes.map { |name, _| [name, read_attribute(name)] }]
|
||||
end
|
||||
|
||||
# Returns an <tt>#inspect</tt>-like string for the value of the
|
||||
# attribute +attr_name+. String attributes are truncated upto 50
|
||||
# characters, and Date and Time attributes are returned in the
|
||||
# <tt>:db</tt> format. Other attributes return the value of
|
||||
# <tt>#inspect</tt> without modification.
|
||||
#
|
||||
# person = Person.create!(:name => "David Heinemeier Hansson " * 3)
|
||||
#
|
||||
# person.attribute_for_inspect(:name)
|
||||
# # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
|
||||
#
|
||||
# person.attribute_for_inspect(:created_at)
|
||||
# # => '"2009-01-12 04:48:57"'
|
||||
def attribute_for_inspect(attr_name)
|
||||
value = read_attribute(attr_name)
|
||||
|
||||
if value.is_a?(String) && value.length > 50
|
||||
"#{value[0..50]}...".inspect
|
||||
elsif value.is_a?(Date) || value.is_a?(Time)
|
||||
%("#{value.to_s(:db)}")
|
||||
else
|
||||
value.inspect
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
|
||||
# nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
|
||||
def attribute_present?(attribute)
|
||||
value = read_attribute(attribute)
|
||||
!value.nil? || (value.respond_to?(:empty?) && !value.empty?)
|
||||
end
|
||||
|
||||
# Returns the column object for the named attribute.
|
||||
def column_for_attribute(name)
|
||||
self.class.columns_hash[name.to_s]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def clone_attributes(reader_method = :read_attribute, attributes = {})
|
||||
attribute_names.each do |name|
|
||||
attributes[name] = clone_attribute_value(reader_method, name)
|
||||
end
|
||||
attributes
|
||||
end
|
||||
|
||||
def clone_attribute_value(reader_method, attribute_name)
|
||||
value = send(reader_method, attribute_name)
|
||||
value.duplicable? ? value.clone : value
|
||||
rescue TypeError, NoMethodError
|
||||
value
|
||||
end
|
||||
|
||||
# Returns a copy of the attributes hash where all the values have been safely quoted for use in
|
||||
# an Arel insert/update method.
|
||||
def arel_attributes_values(include_primary_key = true, include_readonly_attributes = true, attribute_names = @attributes.keys)
|
||||
attrs = {}
|
||||
klass = self.class
|
||||
arel_table = klass.arel_table
|
||||
|
||||
attribute_names.each do |name|
|
||||
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
|
||||
|
||||
if include_readonly_attributes || !self.class.readonly_attributes.include?(name)
|
||||
|
||||
value = if klass.serialized_attributes.include?(name)
|
||||
@attributes[name].serialized_value
|
||||
else
|
||||
# FIXME: we need @attributes to be used consistently.
|
||||
# If the values stored in @attributes were already type
|
||||
# casted, this code could be simplified
|
||||
read_attribute(name)
|
||||
end
|
||||
|
||||
attrs[arel_table[name]] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
def attribute_method?(attr_name)
|
||||
attr_name == 'id' || (defined?(@attributes) && @attributes.include?(attr_name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -49,6 +49,18 @@ module ActiveRecord
|
|||
value
|
||||
end
|
||||
end
|
||||
|
||||
def convert_number_column_value(value)
|
||||
if value == false
|
||||
0
|
||||
elsif value == true
|
||||
1
|
||||
elsif value.is_a?(String) && value.blank?
|
||||
nil
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
File diff suppressed because it is too large
Load diff
138
activerecord/lib/active_record/default_scope.rb
Normal file
138
activerecord/lib/active_record/default_scope.rb
Normal file
|
@ -0,0 +1,138 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module DefaultScope
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Stores the default scope for the class
|
||||
class_attribute :default_scopes, :instance_writer => false
|
||||
self.default_scopes = []
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Returns a scope for this class without taking into account the default_scope.
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# def self.default_scope
|
||||
# where :published => true
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Post.all # Fires "SELECT * FROM posts WHERE published = true"
|
||||
# Post.unscoped.all # Fires "SELECT * FROM posts"
|
||||
#
|
||||
# This method also accepts a block meaning that all queries inside the block will
|
||||
# not use the default_scope:
|
||||
#
|
||||
# Post.unscoped {
|
||||
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
|
||||
# }
|
||||
#
|
||||
# It is recommended to use block form of unscoped because chaining unscoped with <tt>scope</tt>
|
||||
# does not work. Assuming that <tt>published</tt> is a <tt>scope</tt> following two statements are same.
|
||||
#
|
||||
# Post.unscoped.published
|
||||
# Post.published
|
||||
def unscoped #:nodoc:
|
||||
block_given? ? relation.scoping { yield } : relation
|
||||
end
|
||||
|
||||
def before_remove_const #:nodoc:
|
||||
self.current_scope = nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Use this macro in your model to set a default scope for all operations on
|
||||
# the model.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# default_scope where(:published => true)
|
||||
# end
|
||||
#
|
||||
# Article.all # => SELECT * FROM articles WHERE published = true
|
||||
#
|
||||
# The <tt>default_scope</tt> is also applied while creating/building a record. It is not
|
||||
# applied while updating a record.
|
||||
#
|
||||
# Article.new.published # => true
|
||||
# Article.create.published # => true
|
||||
#
|
||||
# You can also use <tt>default_scope</tt> with a block, in order to have it lazily evaluated:
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# default_scope { where(:published_at => Time.now - 1.week) }
|
||||
# end
|
||||
#
|
||||
# (You can also pass any object which responds to <tt>call</tt> to the <tt>default_scope</tt>
|
||||
# macro, and it will be called when building the default scope.)
|
||||
#
|
||||
# If you use multiple <tt>default_scope</tt> declarations in your model then they will
|
||||
# be merged together:
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# default_scope where(:published => true)
|
||||
# default_scope where(:rating => 'G')
|
||||
# end
|
||||
#
|
||||
# Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G'
|
||||
#
|
||||
# This is also the case with inheritance and module includes where the parent or module
|
||||
# defines a <tt>default_scope</tt> and the child or including class defines a second one.
|
||||
#
|
||||
# If you need to do more complex things with a default scope, you can alternatively
|
||||
# define it as a class method:
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.default_scope
|
||||
# # Should return a scope, you can call 'super' here etc.
|
||||
# end
|
||||
# end
|
||||
def default_scope(scope = {})
|
||||
scope = Proc.new if block_given?
|
||||
self.default_scopes = default_scopes + [scope]
|
||||
end
|
||||
|
||||
def build_default_scope #:nodoc:
|
||||
if method(:default_scope).owner != DefaultScope::ClassMethods
|
||||
evaluate_default_scope { default_scope }
|
||||
elsif default_scopes.any?
|
||||
evaluate_default_scope do
|
||||
default_scopes.inject(relation) do |default_scope, scope|
|
||||
if scope.is_a?(Hash)
|
||||
default_scope.apply_finder_options(scope)
|
||||
elsif !scope.is_a?(Relation) && scope.respond_to?(:call)
|
||||
default_scope.merge(scope.call)
|
||||
else
|
||||
default_scope.merge(scope)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ignore_default_scope? #:nodoc:
|
||||
Thread.current["#{self}_ignore_default_scope"]
|
||||
end
|
||||
|
||||
def ignore_default_scope=(ignore) #:nodoc:
|
||||
Thread.current["#{self}_ignore_default_scope"] = ignore
|
||||
end
|
||||
|
||||
# The ignore_default_scope flag is used to prevent an infinite recursion situation where
|
||||
# a default scope references a scope which has a default scope which references a scope...
|
||||
def evaluate_default_scope
|
||||
return if ignore_default_scope?
|
||||
|
||||
begin
|
||||
self.ignore_default_scope = true
|
||||
yield
|
||||
ensure
|
||||
self.ignore_default_scope = false
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
79
activerecord/lib/active_record/dynamic_matchers.rb
Normal file
79
activerecord/lib/active_record/dynamic_matchers.rb
Normal file
|
@ -0,0 +1,79 @@
|
|||
module ActiveRecord
|
||||
module DynamicMatchers
|
||||
def respond_to?(method_id, include_private = false)
|
||||
if match = DynamicFinderMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
elsif match = DynamicScopeMatch.match(method_id)
|
||||
return true if all_attributes_exists?(match.attribute_names)
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Enables dynamic finders like <tt>User.find_by_user_name(user_name)</tt> and
|
||||
# <tt>User.scoped_by_user_name(user_name). Refer to Dynamic attribute-based finders
|
||||
# section at the top of this file for more detailed information.
|
||||
#
|
||||
# It's even possible to use all the additional parameters to +find+. For example, the
|
||||
# full interface for +find_all_by_amount+ is actually <tt>find_all_by_amount(amount, options)</tt>.
|
||||
#
|
||||
# Each dynamic finder using <tt>scoped_by_*</tt> is also defined in the class after it
|
||||
# is first invoked, so that future attempts to use it do not run through method_missing.
|
||||
def method_missing(method_id, *arguments, &block)
|
||||
if match = (DynamicFinderMatch.match(method_id) || DynamicScopeMatch.match(method_id))
|
||||
attribute_names = match.attribute_names
|
||||
super unless all_attributes_exists?(attribute_names)
|
||||
if arguments.size < attribute_names.size
|
||||
method_trace = "#{__FILE__}:#{__LINE__}:in `#{method_id}'"
|
||||
backtrace = [method_trace] + caller
|
||||
raise ArgumentError, "wrong number of arguments (#{arguments.size} for #{attribute_names.size})", backtrace
|
||||
end
|
||||
if match.respond_to?(:scope?) && match.scope?
|
||||
self.class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
||||
def self.#{method_id}(*args) # def self.scoped_by_user_name_and_password(*args)
|
||||
attributes = Hash[[:#{attribute_names.join(',:')}].zip(args)] # attributes = Hash[[:user_name, :password].zip(args)]
|
||||
#
|
||||
scoped(:conditions => attributes) # scoped(:conditions => attributes)
|
||||
end # end
|
||||
METHOD
|
||||
send(method_id, *arguments)
|
||||
elsif match.finder?
|
||||
options = arguments.extract_options!
|
||||
relation = options.any? ? scoped(options) : scoped
|
||||
relation.send :find_by_attributes, match, attribute_names, *arguments, &block
|
||||
elsif match.instantiator?
|
||||
scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
# Similar in purpose to +expand_hash_conditions_for_aggregates+.
|
||||
def expand_attribute_names_for_aggregates(attribute_names)
|
||||
attribute_names.map { |attribute_name|
|
||||
unless (aggregation = reflect_on_aggregation(attribute_name.to_sym)).nil?
|
||||
aggregate_mapping(aggregation).map do |field_attr, _|
|
||||
field_attr.to_sym
|
||||
end
|
||||
else
|
||||
attribute_name.to_sym
|
||||
end
|
||||
}.flatten
|
||||
end
|
||||
|
||||
def all_attributes_exists?(attribute_names)
|
||||
(expand_attribute_names_for_aggregates(attribute_names) -
|
||||
column_methods_hash.keys).empty?
|
||||
end
|
||||
|
||||
def aggregate_mapping(reflection)
|
||||
mapping = reflection.options[:mapping] || [reflection.name, reflection.name]
|
||||
mapping.first.is_a?(Array) ? mapping : [mapping]
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
end
|
|
@ -1,72 +1,85 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module Explain
|
||||
# If auto explain is enabled, this method triggers EXPLAIN logging for the
|
||||
# queries triggered by the block if it takes more than the threshold as a
|
||||
# whole. That is, the threshold is not checked against each individual
|
||||
# query, but against the duration of the entire block. This approach is
|
||||
# convenient for relations.
|
||||
#
|
||||
# The available_queries_for_explain thread variable collects the queries
|
||||
# to be explained. If the value is nil, it means queries are not being
|
||||
# currently collected. A false value indicates collecting is turned
|
||||
# off. Otherwise it is an array of queries.
|
||||
def logging_query_plan # :nodoc:
|
||||
threshold = auto_explain_threshold_in_seconds
|
||||
current = Thread.current
|
||||
if threshold && current[:available_queries_for_explain].nil?
|
||||
begin
|
||||
queries = current[:available_queries_for_explain] = []
|
||||
start = Time.now
|
||||
result = yield
|
||||
logger.warn(exec_explain(queries)) if Time.now - start > threshold
|
||||
result
|
||||
ensure
|
||||
current[:available_queries_for_explain] = nil
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# If a query takes longer than these many seconds we log its query plan
|
||||
# automatically. nil disables this feature.
|
||||
class_attribute :auto_explain_threshold_in_seconds, :instance_writer => false
|
||||
self.auto_explain_threshold_in_seconds = nil
|
||||
end
|
||||
|
||||
# Relation#explain needs to be able to collect the queries regardless of
|
||||
# whether auto explain is enabled. This method serves that purpose.
|
||||
def collecting_queries_for_explain # :nodoc:
|
||||
current = Thread.current
|
||||
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
|
||||
return yield, current[:available_queries_for_explain]
|
||||
ensure
|
||||
# Note that the return value above does not depend on this assigment.
|
||||
current[:available_queries_for_explain] = original
|
||||
end
|
||||
|
||||
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
|
||||
# Returns a formatted string ready to be logged.
|
||||
def exec_explain(queries) # :nodoc:
|
||||
queries && queries.map do |sql, bind|
|
||||
[].tap do |msg|
|
||||
msg << "EXPLAIN for: #{sql}"
|
||||
unless bind.empty?
|
||||
bind_msg = bind.map {|col, val| [col.name, val]}.inspect
|
||||
msg.last << " #{bind_msg}"
|
||||
module ClassMethods
|
||||
# If auto explain is enabled, this method triggers EXPLAIN logging for the
|
||||
# queries triggered by the block if it takes more than the threshold as a
|
||||
# whole. That is, the threshold is not checked against each individual
|
||||
# query, but against the duration of the entire block. This approach is
|
||||
# convenient for relations.
|
||||
#
|
||||
# The available_queries_for_explain thread variable collects the queries
|
||||
# to be explained. If the value is nil, it means queries are not being
|
||||
# currently collected. A false value indicates collecting is turned
|
||||
# off. Otherwise it is an array of queries.
|
||||
def logging_query_plan # :nodoc:
|
||||
threshold = auto_explain_threshold_in_seconds
|
||||
current = Thread.current
|
||||
if threshold && current[:available_queries_for_explain].nil?
|
||||
begin
|
||||
queries = current[:available_queries_for_explain] = []
|
||||
start = Time.now
|
||||
result = yield
|
||||
logger.warn(exec_explain(queries)) if Time.now - start > threshold
|
||||
result
|
||||
ensure
|
||||
current[:available_queries_for_explain] = nil
|
||||
end
|
||||
msg << connection.explain(sql, bind)
|
||||
end.join("\n")
|
||||
end.join("\n")
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Silences automatic EXPLAIN logging for the duration of the block.
|
||||
#
|
||||
# This has high priority, no EXPLAINs will be run even if downwards
|
||||
# the threshold is set to 0.
|
||||
#
|
||||
# As the name of the method suggests this only applies to automatic
|
||||
# EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
|
||||
def silence_auto_explain
|
||||
current = Thread.current
|
||||
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
|
||||
yield
|
||||
ensure
|
||||
current[:available_queries_for_explain] = original
|
||||
# Relation#explain needs to be able to collect the queries regardless of
|
||||
# whether auto explain is enabled. This method serves that purpose.
|
||||
def collecting_queries_for_explain # :nodoc:
|
||||
current = Thread.current
|
||||
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
|
||||
return yield, current[:available_queries_for_explain]
|
||||
ensure
|
||||
# Note that the return value above does not depend on this assigment.
|
||||
current[:available_queries_for_explain] = original
|
||||
end
|
||||
|
||||
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
|
||||
# Returns a formatted string ready to be logged.
|
||||
def exec_explain(queries) # :nodoc:
|
||||
queries && queries.map do |sql, bind|
|
||||
[].tap do |msg|
|
||||
msg << "EXPLAIN for: #{sql}"
|
||||
unless bind.empty?
|
||||
bind_msg = bind.map {|col, val| [col.name, val]}.inspect
|
||||
msg.last << " #{bind_msg}"
|
||||
end
|
||||
msg << connection.explain(sql, bind)
|
||||
end.join("\n")
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
# Silences automatic EXPLAIN logging for the duration of the block.
|
||||
#
|
||||
# This has high priority, no EXPLAINs will be run even if downwards
|
||||
# the threshold is set to 0.
|
||||
#
|
||||
# As the name of the method suggests this only applies to automatic
|
||||
# EXPLAINs, manual calls to +ActiveRecord::Relation#explain+ run.
|
||||
def silence_auto_explain
|
||||
current = Thread.current
|
||||
original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
|
||||
yield
|
||||
ensure
|
||||
current[:available_queries_for_explain] = original
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
167
activerecord/lib/active_record/inheritance.rb
Normal file
167
activerecord/lib/active_record/inheritance.rb
Normal file
|
@ -0,0 +1,167 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module Inheritance
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Determine whether to store the full constant name including namespace when using STI
|
||||
class_attribute :store_full_sti_class
|
||||
self.store_full_sti_class = true
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# True if this isn't a concrete subclass needing a STI type condition.
|
||||
def descends_from_active_record?
|
||||
if superclass.abstract_class?
|
||||
superclass.descends_from_active_record?
|
||||
else
|
||||
superclass == Base || !columns_hash.include?(inheritance_column)
|
||||
end
|
||||
end
|
||||
|
||||
def finder_needs_type_condition? #:nodoc:
|
||||
# This is like this because benchmarking justifies the strange :false stuff
|
||||
:true == (@finder_needs_type_condition ||= descends_from_active_record? ? :false : :true)
|
||||
end
|
||||
|
||||
def symbolized_base_class
|
||||
@symbolized_base_class ||= base_class.to_s.to_sym
|
||||
end
|
||||
|
||||
def symbolized_sti_name
|
||||
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
|
||||
end
|
||||
|
||||
# Returns the base AR subclass that this class descends from. If A
|
||||
# extends AR::Base, A.base_class will return A. If B descends from A
|
||||
# through some arbitrarily deep hierarchy, B.base_class will return A.
|
||||
#
|
||||
# If B < A and C < B and if A is an abstract_class then both B.base_class
|
||||
# and C.base_class would return B as the answer since A is an abstract_class.
|
||||
def base_class
|
||||
class_of_active_record_descendant(self)
|
||||
end
|
||||
|
||||
# Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
|
||||
attr_accessor :abstract_class
|
||||
|
||||
# Returns whether this class is an abstract class or not.
|
||||
def abstract_class?
|
||||
defined?(@abstract_class) && @abstract_class == true
|
||||
end
|
||||
|
||||
def sti_name
|
||||
store_full_sti_class ? name : name.demodulize
|
||||
end
|
||||
|
||||
# Finder methods must instantiate through this method to work with the
|
||||
# single-table inheritance model that makes it possible to create
|
||||
# objects of different types from the same table.
|
||||
def instantiate(record)
|
||||
sti_class = find_sti_class(record[inheritance_column])
|
||||
record_id = sti_class.primary_key && record[sti_class.primary_key]
|
||||
|
||||
if ActiveRecord::IdentityMap.enabled? && record_id
|
||||
if (column = sti_class.columns_hash[sti_class.primary_key]) && column.number?
|
||||
record_id = record_id.to_i
|
||||
end
|
||||
if instance = IdentityMap.get(sti_class, record_id)
|
||||
instance.reinit_with('attributes' => record)
|
||||
else
|
||||
instance = sti_class.allocate.init_with('attributes' => record)
|
||||
IdentityMap.add(instance)
|
||||
end
|
||||
else
|
||||
instance = sti_class.allocate.init_with('attributes' => record)
|
||||
end
|
||||
|
||||
instance
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Returns the class descending directly from ActiveRecord::Base or an
|
||||
# abstract class, if any, in the inheritance hierarchy.
|
||||
def class_of_active_record_descendant(klass)
|
||||
if klass == Base || klass.superclass == Base || klass.superclass.abstract_class?
|
||||
klass
|
||||
elsif klass.superclass.nil?
|
||||
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
|
||||
else
|
||||
class_of_active_record_descendant(klass.superclass)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the class type of the record using the current module as a prefix. So descendants of
|
||||
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
|
||||
def compute_type(type_name)
|
||||
if type_name.match(/^::/)
|
||||
# If the type is prefixed with a scope operator then we assume that
|
||||
# the type_name is an absolute reference.
|
||||
ActiveSupport::Dependencies.constantize(type_name)
|
||||
else
|
||||
# Build a list of candidates to search for
|
||||
candidates = []
|
||||
name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" }
|
||||
candidates << type_name
|
||||
|
||||
candidates.each do |candidate|
|
||||
begin
|
||||
constant = ActiveSupport::Dependencies.constantize(candidate)
|
||||
return constant if candidate == constant.to_s
|
||||
rescue NameError => e
|
||||
# We don't want to swallow NoMethodError < NameError errors
|
||||
raise e unless e.instance_of?(NameError)
|
||||
end
|
||||
end
|
||||
|
||||
raise NameError, "uninitialized constant #{candidates.first}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_sti_class(type_name)
|
||||
if type_name.blank? || !columns_hash.include?(inheritance_column)
|
||||
self
|
||||
else
|
||||
begin
|
||||
if store_full_sti_class
|
||||
ActiveSupport::Dependencies.constantize(type_name)
|
||||
else
|
||||
compute_type(type_name)
|
||||
end
|
||||
rescue NameError
|
||||
raise SubclassNotFound,
|
||||
"The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
|
||||
"This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
|
||||
"Please rename this column if you didn't intend it to be used for storing the inheritance class " +
|
||||
"or overwrite #{name}.inheritance_column to use another column for that information."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def type_condition(table = arel_table)
|
||||
sti_column = table[inheritance_column.to_sym]
|
||||
sti_names = ([self] + descendants).map { |model| model.sti_name }
|
||||
|
||||
sti_column.in(sti_names)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Sets the attribute used for single table inheritance to this class name if this is not the
|
||||
# ActiveRecord::Base descendant.
|
||||
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
|
||||
# do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself.
|
||||
# No such attribute would be set for objects of the Message class in that example.
|
||||
def ensure_proper_type
|
||||
klass = self.class
|
||||
if klass.finder_needs_type_condition?
|
||||
write_attribute(klass.inheritance_column, klass.sti_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
49
activerecord/lib/active_record/integration.rb
Normal file
49
activerecord/lib/active_record/integration.rb
Normal file
|
@ -0,0 +1,49 @@
|
|||
module ActiveRecord
|
||||
module Integration
|
||||
# Returns a String, which Action Pack uses for constructing an URL to this
|
||||
# object. The default implementation returns this record's id as a String,
|
||||
# or nil if this record's unsaved.
|
||||
#
|
||||
# For example, suppose that you have a User model, and that you have a
|
||||
# <tt>resources :users</tt> route. Normally, +user_path+ will
|
||||
# construct a path with the user object's 'id' in it:
|
||||
#
|
||||
# user = User.find_by_name('Phusion')
|
||||
# user_path(user) # => "/users/1"
|
||||
#
|
||||
# You can override +to_param+ in your model to make +user_path+ construct
|
||||
# a path using the user's name instead of the user's id:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# def to_param # overridden
|
||||
# name
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# user = User.find_by_name('Phusion')
|
||||
# user_path(user) # => "/users/Phusion"
|
||||
def to_param
|
||||
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
|
||||
id && id.to_s # Be sure to stringify the id for routes
|
||||
end
|
||||
|
||||
# Returns a cache key that can be used to identify this record.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Product.new.cache_key # => "products/new"
|
||||
# Product.find(5).cache_key # => "products/5" (updated_at not available)
|
||||
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
|
||||
def cache_key
|
||||
case
|
||||
when new_record?
|
||||
"#{self.class.model_name.cache_key}/new"
|
||||
when timestamp = self[:updated_at]
|
||||
timestamp = timestamp.utc.to_s(:number)
|
||||
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
|
||||
else
|
||||
"#{self.class.model_name.cache_key}/#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
361
activerecord/lib/active_record/model_schema.rb
Normal file
361
activerecord/lib/active_record/model_schema.rb
Normal file
|
@ -0,0 +1,361 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module ModelSchema
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
##
|
||||
# :singleton-method:
|
||||
# Accessor for the prefix type that will be prepended to every primary key column name.
|
||||
# The options are :table_name and :table_name_with_underscore. If the first is specified,
|
||||
# the Product class will look for "productid" instead of "id" as the primary column. If the
|
||||
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
|
||||
# that this is a global setting for all Active Records.
|
||||
cattr_accessor :primary_key_prefix_type, :instance_writer => false
|
||||
self.primary_key_prefix_type = nil
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Accessor for the name of the prefix string to prepend to every table name. So if set
|
||||
# to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people",
|
||||
# etc. This is a convenient way of creating a namespace for tables in a shared database.
|
||||
# By default, the prefix is the empty string.
|
||||
#
|
||||
# If you are organising your models within modules you can add a prefix to the models within
|
||||
# a namespace by defining a singleton method in the parent module called table_name_prefix which
|
||||
# returns your chosen prefix.
|
||||
class_attribute :table_name_prefix, :instance_writer => false
|
||||
self.table_name_prefix = ""
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
|
||||
# "people_basecamp"). By default, the suffix is the empty string.
|
||||
class_attribute :table_name_suffix, :instance_writer => false
|
||||
self.table_name_suffix = ""
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
# Indicates whether table names should be the pluralized versions of the corresponding class names.
|
||||
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
|
||||
# See table_name for the full rules on table/class naming. This is true, by default.
|
||||
class_attribute :pluralize_table_names, :instance_writer => false
|
||||
self.pluralize_table_names = true
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Guesses the table name (in forced lower-case) based on the name of the class in the
|
||||
# inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy
|
||||
# looks like: Reply < Message < ActiveRecord::Base, then Message is used
|
||||
# to guess the table name even when called on Reply. The rules used to do the guess
|
||||
# are handled by the Inflector class in Active Support, which knows almost all common
|
||||
# English inflections. You can add new inflections in config/initializers/inflections.rb.
|
||||
#
|
||||
# Nested classes are given table names prefixed by the singular form of
|
||||
# the parent's table name. Enclosing modules are not considered.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# class Invoice < ActiveRecord::Base
|
||||
# end
|
||||
#
|
||||
# file class table_name
|
||||
# invoice.rb Invoice invoices
|
||||
#
|
||||
# class Invoice < ActiveRecord::Base
|
||||
# class Lineitem < ActiveRecord::Base
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# file class table_name
|
||||
# invoice.rb Invoice::Lineitem invoice_lineitems
|
||||
#
|
||||
# module Invoice
|
||||
# class Lineitem < ActiveRecord::Base
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# file class table_name
|
||||
# invoice/lineitem.rb Invoice::Lineitem lineitems
|
||||
#
|
||||
# Additionally, the class-level +table_name_prefix+ is prepended and the
|
||||
# +table_name_suffix+ is appended. So if you have "myapp_" as a prefix,
|
||||
# the table name guess for an Invoice class becomes "myapp_invoices".
|
||||
# Invoice::Lineitem becomes "myapp_invoice_lineitems".
|
||||
#
|
||||
# You can also set your own table name explicitly:
|
||||
#
|
||||
# class Mouse < ActiveRecord::Base
|
||||
# self.table_name = "mice"
|
||||
# end
|
||||
#
|
||||
# Alternatively, you can override the table_name method to define your
|
||||
# own computation. (Possibly using <tt>super</tt> to manipulate the default
|
||||
# table name.) Example:
|
||||
#
|
||||
# class Post < ActiveRecord::Base
|
||||
# def self.table_name
|
||||
# "special_" + super
|
||||
# end
|
||||
# end
|
||||
# Post.table_name # => "special_posts"
|
||||
def table_name
|
||||
reset_table_name unless defined?(@table_name)
|
||||
@table_name
|
||||
end
|
||||
|
||||
def original_table_name #:nodoc:
|
||||
deprecated_original_property_getter :table_name
|
||||
end
|
||||
|
||||
# Sets the table name explicitly. Example:
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# self.table_name = "project"
|
||||
# end
|
||||
#
|
||||
# You can also just define your own <tt>self.table_name</tt> method; see
|
||||
# the documentation for ActiveRecord::Base#table_name.
|
||||
def table_name=(value)
|
||||
@original_table_name = @table_name if defined?(@table_name)
|
||||
@table_name = value
|
||||
@quoted_table_name = nil
|
||||
@arel_table = nil
|
||||
@relation = Relation.new(self, arel_table)
|
||||
end
|
||||
|
||||
def set_table_name(value = nil, &block) #:nodoc:
|
||||
deprecated_property_setter :table_name, value, block
|
||||
@quoted_table_name = nil
|
||||
@arel_table = nil
|
||||
@relation = Relation.new(self, arel_table)
|
||||
end
|
||||
|
||||
# Returns a quoted version of the table name, used to construct SQL statements.
|
||||
def quoted_table_name
|
||||
@quoted_table_name ||= connection.quote_table_name(table_name)
|
||||
end
|
||||
|
||||
# Computes the table name, (re)sets it internally, and returns it.
|
||||
def reset_table_name #:nodoc:
|
||||
if superclass.abstract_class?
|
||||
self.table_name = superclass.table_name || compute_table_name
|
||||
elsif abstract_class?
|
||||
self.table_name = superclass == Base ? nil : superclass.table_name
|
||||
else
|
||||
self.table_name = compute_table_name
|
||||
end
|
||||
end
|
||||
|
||||
def full_table_name_prefix #:nodoc:
|
||||
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
|
||||
end
|
||||
|
||||
# The name of the column containing the object's class when Single Table Inheritance is used
|
||||
def inheritance_column
|
||||
if self == Base
|
||||
'type'
|
||||
else
|
||||
(@inheritance_column ||= nil) || superclass.inheritance_column
|
||||
end
|
||||
end
|
||||
|
||||
def original_inheritance_column #:nodoc:
|
||||
deprecated_original_property_getter :inheritance_column
|
||||
end
|
||||
|
||||
# Sets the value of inheritance_column
|
||||
def inheritance_column=(value)
|
||||
@original_inheritance_column = inheritance_column
|
||||
@inheritance_column = value.to_s
|
||||
end
|
||||
|
||||
def set_inheritance_column(value = nil, &block) #:nodoc:
|
||||
deprecated_property_setter :inheritance_column, value, block
|
||||
end
|
||||
|
||||
def sequence_name
|
||||
if base_class == self
|
||||
@sequence_name ||= reset_sequence_name
|
||||
else
|
||||
(@sequence_name ||= nil) || base_class.sequence_name
|
||||
end
|
||||
end
|
||||
|
||||
def original_sequence_name #:nodoc:
|
||||
deprecated_original_property_getter :sequence_name
|
||||
end
|
||||
|
||||
def reset_sequence_name #:nodoc:
|
||||
self.sequence_name = connection.default_sequence_name(table_name, primary_key)
|
||||
end
|
||||
|
||||
# Sets the name of the sequence to use when generating ids to the given
|
||||
# value, or (if the value is nil or false) to the value returned by the
|
||||
# given block. This is required for Oracle and is useful for any
|
||||
# database which relies on sequences for primary key generation.
|
||||
#
|
||||
# If a sequence name is not explicitly set when using Oracle or Firebird,
|
||||
# it will default to the commonly used pattern of: #{table_name}_seq
|
||||
#
|
||||
# If a sequence name is not explicitly set when using PostgreSQL, it
|
||||
# will discover the sequence corresponding to your primary key for you.
|
||||
#
|
||||
# class Project < ActiveRecord::Base
|
||||
# self.sequence_name = "projectseq" # default would have been "project_seq"
|
||||
# end
|
||||
def sequence_name=(value)
|
||||
@original_sequence_name = @sequence_name if defined?(@sequence_name)
|
||||
@sequence_name = value.to_s
|
||||
end
|
||||
|
||||
def set_sequence_name(value = nil, &block) #:nodoc:
|
||||
deprecated_property_setter :sequence_name, value, block
|
||||
end
|
||||
|
||||
# Indicates whether the table associated with this class exists
|
||||
def table_exists?
|
||||
connection.schema_cache.table_exists?(table_name)
|
||||
end
|
||||
|
||||
# Returns an array of column objects for the table associated with this class.
|
||||
def columns
|
||||
if defined?(@primary_key)
|
||||
connection.schema_cache.primary_keys[table_name] ||= primary_key
|
||||
end
|
||||
|
||||
connection.schema_cache.columns[table_name]
|
||||
end
|
||||
|
||||
# Returns a hash of column objects for the table associated with this class.
|
||||
def columns_hash
|
||||
connection.schema_cache.columns_hash[table_name]
|
||||
end
|
||||
|
||||
# Returns a hash where the keys are column names and the values are
|
||||
# default values when instantiating the AR object for this table.
|
||||
def column_defaults
|
||||
@column_defaults ||= connection.schema_cache.column_defaults[table_name]
|
||||
end
|
||||
|
||||
# Returns an array of column names as strings.
|
||||
def column_names
|
||||
@column_names ||= columns.map { |column| column.name }
|
||||
end
|
||||
|
||||
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
|
||||
# and columns used for single table inheritance have been removed.
|
||||
def content_columns
|
||||
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
|
||||
end
|
||||
|
||||
# Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
|
||||
# and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
|
||||
# is available.
|
||||
def column_methods_hash #:nodoc:
|
||||
@dynamic_methods_hash ||= column_names.inject(Hash.new(false)) do |methods, attr|
|
||||
attr_name = attr.to_s
|
||||
methods[attr.to_sym] = attr_name
|
||||
methods["#{attr}=".to_sym] = attr_name
|
||||
methods["#{attr}?".to_sym] = attr_name
|
||||
methods["#{attr}_before_type_cast".to_sym] = attr_name
|
||||
methods
|
||||
end
|
||||
end
|
||||
|
||||
# Resets all the cached information about columns, which will cause them
|
||||
# to be reloaded on the next request.
|
||||
#
|
||||
# The most common usage pattern for this method is probably in a migration,
|
||||
# when just after creating a table you want to populate it with some default
|
||||
# values, eg:
|
||||
#
|
||||
# class CreateJobLevels < ActiveRecord::Migration
|
||||
# def up
|
||||
# create_table :job_levels do |t|
|
||||
# t.integer :id
|
||||
# t.string :name
|
||||
#
|
||||
# t.timestamps
|
||||
# end
|
||||
#
|
||||
# JobLevel.reset_column_information
|
||||
# %w{assistant executive manager director}.each do |type|
|
||||
# JobLevel.create(:name => type)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def down
|
||||
# drop_table :job_levels
|
||||
# end
|
||||
# end
|
||||
def reset_column_information
|
||||
connection.clear_cache!
|
||||
undefine_attribute_methods
|
||||
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
|
||||
|
||||
@column_names = @content_columns = @column_defaults = @dynamic_methods_hash = @inheritance_column = nil
|
||||
@arel_engine = @relation = nil
|
||||
end
|
||||
|
||||
def clear_cache! # :nodoc:
|
||||
connection.schema_cache.clear!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Guesses the table name, but does not decorate it with prefix and suffix information.
|
||||
def undecorated_table_name(class_name = base_class.name)
|
||||
table_name = class_name.to_s.demodulize.underscore
|
||||
table_name = table_name.pluralize if pluralize_table_names
|
||||
table_name
|
||||
end
|
||||
|
||||
# Computes and returns a table name according to default conventions.
|
||||
def compute_table_name
|
||||
base = base_class
|
||||
if self == base
|
||||
# Nested classes are prefixed with singular parent table name.
|
||||
if parent < ActiveRecord::Base && !parent.abstract_class?
|
||||
contained = parent.table_name
|
||||
contained = contained.singularize if parent.pluralize_table_names
|
||||
contained += '_'
|
||||
end
|
||||
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
|
||||
else
|
||||
# STI subclasses always use their superclass' table.
|
||||
base.table_name
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_property_setter(property, value, block)
|
||||
if block
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"Calling set_#{property} is deprecated. If you need to lazily evaluate " \
|
||||
"the #{property}, define your own `self.#{property}` class method. You can use `super` " \
|
||||
"to get the default #{property} where you would have called `original_#{property}`."
|
||||
)
|
||||
|
||||
define_attr_method property, value, false, &block
|
||||
else
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"Calling set_#{property} is deprecated. Please use `self.#{property} = 'the_name'` instead."
|
||||
)
|
||||
|
||||
define_attr_method property, value, false
|
||||
end
|
||||
end
|
||||
|
||||
def deprecated_original_property_getter(property)
|
||||
ActiveSupport::Deprecation.warn("original_#{property} is deprecated. Define self.#{property} and call super instead.")
|
||||
|
||||
if !instance_variable_defined?("@original_#{property}") && respond_to?("reset_#{property}")
|
||||
send("reset_#{property}")
|
||||
else
|
||||
instance_variable_get("@original_#{property}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,53 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
# = Active Record Persistence
|
||||
module Persistence
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# Creates an object (or multiple objects) and saves it to the database, if validations pass.
|
||||
# The resulting object is returned whether the object was saved successfully to the database or not.
|
||||
#
|
||||
# The +attributes+ parameter can be either be a Hash or an Array of Hashes. These Hashes describe the
|
||||
# attributes on the objects that are to be created.
|
||||
#
|
||||
# +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
|
||||
# in the +options+ parameter.
|
||||
#
|
||||
# ==== Examples
|
||||
# # Create a single new object
|
||||
# User.create(:first_name => 'Jamie')
|
||||
#
|
||||
# # Create a single new object using the :admin mass-assignment security role
|
||||
# User.create({ :first_name => 'Jamie', :is_admin => true }, :as => :admin)
|
||||
#
|
||||
# # Create a single new object bypassing mass-assignment security
|
||||
# User.create({ :first_name => 'Jamie', :is_admin => true }, :without_protection => true)
|
||||
#
|
||||
# # Create an Array of new objects
|
||||
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
|
||||
#
|
||||
# # Create a single object and pass it into a block to set other attributes.
|
||||
# User.create(:first_name => 'Jamie') do |u|
|
||||
# u.is_admin = false
|
||||
# end
|
||||
#
|
||||
# # Creating an Array of new objects using a block, where the block is executed for each object:
|
||||
# User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
|
||||
# u.is_admin = false
|
||||
# end
|
||||
def create(attributes = nil, options = {}, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| create(attr, options, &block) }
|
||||
else
|
||||
object = new(attributes, options, &block)
|
||||
object.save
|
||||
object
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if this object hasn't been saved yet -- that is, a record
|
||||
# for the object doesn't exist in the data store yet; otherwise, returns false.
|
||||
def new_record?
|
||||
|
|
58
activerecord/lib/active_record/querying.rb
Normal file
58
activerecord/lib/active_record/querying.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
require 'active_support/core_ext/module/delegation'
|
||||
|
||||
module ActiveRecord
|
||||
module Querying
|
||||
delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped
|
||||
delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped
|
||||
delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped
|
||||
delegate :find_each, :find_in_batches, :to => :scoped
|
||||
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
|
||||
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
|
||||
:having, :create_with, :uniq, :to => :scoped
|
||||
delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :to => :scoped
|
||||
|
||||
# Executes a custom SQL query against your database and returns all the results. The results will
|
||||
# be returned as an array with columns requested encapsulated as attributes of the model you call
|
||||
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
|
||||
# a Product object with the attributes you specified in the SQL query.
|
||||
#
|
||||
# If you call a complicated SQL query which spans multiple tables the columns specified by the
|
||||
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
|
||||
# table.
|
||||
#
|
||||
# The +sql+ parameter is a full SQL query as a string. It will be called as is, there will be
|
||||
# no database agnostic conversions performed. This should be a last resort because using, for example,
|
||||
# MySQL specific terms will lock you to using that particular database engine or require you to
|
||||
# change your call if you switch engines.
|
||||
#
|
||||
# ==== Examples
|
||||
# # A simple SQL query spanning multiple tables
|
||||
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
|
||||
# > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
|
||||
#
|
||||
# # You can use the same string replacement techniques as you can with ActiveRecord#find
|
||||
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
|
||||
# > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
|
||||
def find_by_sql(sql, binds = [])
|
||||
logging_query_plan do
|
||||
connection.select_all(sanitize_sql(sql), "#{name} Load", binds).collect! { |record| instantiate(record) }
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
|
||||
# The use of this method should be restricted to complicated SQL queries that can't be executed
|
||||
# using the ActiveRecord::Calculations class methods. Look into those before using this.
|
||||
#
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +sql+ - An SQL statement which should return a count query from the database, see the example below.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
|
||||
def count_by_sql(sql)
|
||||
sql = sanitize_conditions(sql)
|
||||
connection.select_value(sql, "#{name} Count").to_i
|
||||
end
|
||||
end
|
||||
end
|
26
activerecord/lib/active_record/readonly_attributes.rb
Normal file
26
activerecord/lib/active_record/readonly_attributes.rb
Normal file
|
@ -0,0 +1,26 @@
|
|||
require 'active_support/concern'
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
|
||||
module ActiveRecord
|
||||
module ReadonlyAttributes
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
class_attribute :_attr_readonly, :instance_writer => false
|
||||
self._attr_readonly = []
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Attributes listed as readonly will be used to create a new record but update operations will
|
||||
# ignore these fields.
|
||||
def attr_readonly(*attributes)
|
||||
self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
|
||||
end
|
||||
|
||||
# Returns an array of all the attributes that have been specified as readonly.
|
||||
def readonly_attributes
|
||||
self._attr_readonly
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ module ActiveRecord
|
|||
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
|
||||
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reorder, :reverse_order, :uniq]
|
||||
|
||||
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
|
||||
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain::ClassMethods, Delegation
|
||||
|
||||
attr_reader :table, :klass, :loaded
|
||||
attr_accessor :extensions, :default_scoped
|
||||
|
|
194
activerecord/lib/active_record/sanitization.rb
Normal file
194
activerecord/lib/active_record/sanitization.rb
Normal file
|
@ -0,0 +1,194 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module Sanitization
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
def quote_value(value, column = nil) #:nodoc:
|
||||
connection.quote(value,column)
|
||||
end
|
||||
|
||||
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
|
||||
def sanitize(object) #:nodoc:
|
||||
connection.quote(object)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Accepts an array, hash, or string of SQL conditions and sanitizes
|
||||
# them into a valid SQL fragment for a WHERE clause.
|
||||
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
|
||||
# { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
|
||||
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
|
||||
def sanitize_sql_for_conditions(condition, table_name = self.table_name)
|
||||
return nil if condition.blank?
|
||||
|
||||
case condition
|
||||
when Array; sanitize_sql_array(condition)
|
||||
when Hash; sanitize_sql_hash_for_conditions(condition, table_name)
|
||||
else condition
|
||||
end
|
||||
end
|
||||
alias_method :sanitize_sql, :sanitize_sql_for_conditions
|
||||
|
||||
# Accepts an array, hash, or string of SQL conditions and sanitizes
|
||||
# them into a valid SQL fragment for a SET clause.
|
||||
# { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
|
||||
def sanitize_sql_for_assignment(assignments)
|
||||
case assignments
|
||||
when Array; sanitize_sql_array(assignments)
|
||||
when Hash; sanitize_sql_hash_for_assignment(assignments)
|
||||
else assignments
|
||||
end
|
||||
end
|
||||
|
||||
# Accepts a hash of SQL conditions and replaces those attributes
|
||||
# that correspond to a +composed_of+ relationship with their expanded
|
||||
# aggregate attribute values.
|
||||
# Given:
|
||||
# class Person < ActiveRecord::Base
|
||||
# composed_of :address, :class_name => "Address",
|
||||
# :mapping => [%w(address_street street), %w(address_city city)]
|
||||
# end
|
||||
# Then:
|
||||
# { :address => Address.new("813 abc st.", "chicago") }
|
||||
# # => { :address_street => "813 abc st.", :address_city => "chicago" }
|
||||
def expand_hash_conditions_for_aggregates(attrs)
|
||||
expanded_attrs = {}
|
||||
attrs.each do |attr, value|
|
||||
unless (aggregation = reflect_on_aggregation(attr.to_sym)).nil?
|
||||
mapping = aggregate_mapping(aggregation)
|
||||
mapping.each do |field_attr, aggregate_attr|
|
||||
if mapping.size == 1 && !value.respond_to?(aggregate_attr)
|
||||
expanded_attrs[field_attr] = value
|
||||
else
|
||||
expanded_attrs[field_attr] = value.send(aggregate_attr)
|
||||
end
|
||||
end
|
||||
else
|
||||
expanded_attrs[attr] = value
|
||||
end
|
||||
end
|
||||
expanded_attrs
|
||||
end
|
||||
|
||||
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
|
||||
# { :name => "foo'bar", :group_id => 4 }
|
||||
# # => "name='foo''bar' and group_id= 4"
|
||||
# { :status => nil, :group_id => [1,2,3] }
|
||||
# # => "status IS NULL and group_id IN (1,2,3)"
|
||||
# { :age => 13..18 }
|
||||
# # => "age BETWEEN 13 AND 18"
|
||||
# { 'other_records.id' => 7 }
|
||||
# # => "`other_records`.`id` = 7"
|
||||
# { :other_records => { :id => 7 } }
|
||||
# # => "`other_records`.`id` = 7"
|
||||
# And for value objects on a composed_of relationship:
|
||||
# { :address => Address.new("123 abc st.", "chicago") }
|
||||
# # => "address_street='123 abc st.' and address_city='chicago'"
|
||||
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
|
||||
attrs = expand_hash_conditions_for_aggregates(attrs)
|
||||
|
||||
table = Arel::Table.new(table_name).alias(default_table_name)
|
||||
PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b|
|
||||
connection.visitor.accept b
|
||||
}.join(' AND ')
|
||||
end
|
||||
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
|
||||
|
||||
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
|
||||
# { :status => nil, :group_id => 1 }
|
||||
# # => "status = NULL , group_id = 1"
|
||||
def sanitize_sql_hash_for_assignment(attrs)
|
||||
attrs.map do |attr, value|
|
||||
"#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
|
||||
end.join(', ')
|
||||
end
|
||||
|
||||
# Accepts an array of conditions. The array has each value
|
||||
# sanitized and interpolated into the SQL statement.
|
||||
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
|
||||
def sanitize_sql_array(ary)
|
||||
statement, *values = ary
|
||||
if values.first.is_a?(Hash) && statement =~ /:\w+/
|
||||
replace_named_bind_variables(statement, values.first)
|
||||
elsif statement.include?('?')
|
||||
replace_bind_variables(statement, values)
|
||||
elsif statement.blank?
|
||||
statement
|
||||
else
|
||||
statement % values.collect { |value| connection.quote_string(value.to_s) }
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :sanitize_conditions, :sanitize_sql
|
||||
|
||||
def replace_bind_variables(statement, values) #:nodoc:
|
||||
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
|
||||
bound = values.dup
|
||||
c = connection
|
||||
statement.gsub('?') { quote_bound_value(bound.shift, c) }
|
||||
end
|
||||
|
||||
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
|
||||
statement.gsub(/(:?):([a-zA-Z]\w*)/) do
|
||||
if $1 == ':' # skip postgresql casts
|
||||
$& # return the whole match
|
||||
elsif bind_vars.include?(match = $2.to_sym)
|
||||
quote_bound_value(bind_vars[match])
|
||||
else
|
||||
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expand_range_bind_variables(bind_vars) #:nodoc:
|
||||
expanded = []
|
||||
|
||||
bind_vars.each do |var|
|
||||
next if var.is_a?(Hash)
|
||||
|
||||
if var.is_a?(Range)
|
||||
expanded << var.first
|
||||
expanded << var.last
|
||||
else
|
||||
expanded << var
|
||||
end
|
||||
end
|
||||
|
||||
expanded
|
||||
end
|
||||
|
||||
def quote_bound_value(value, c = connection) #:nodoc:
|
||||
if value.respond_to?(:map) && !value.acts_like?(:string)
|
||||
if value.respond_to?(:empty?) && value.empty?
|
||||
c.quote(nil)
|
||||
else
|
||||
value.map { |v| c.quote(v) }.join(',')
|
||||
end
|
||||
else
|
||||
c.quote(value)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
|
||||
unless expected == provided
|
||||
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate this
|
||||
def quoted_id #:nodoc:
|
||||
quote_value(id, column_for_attribute(self.class.primary_key))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Quote strings appropriately for SQL statements.
|
||||
def quote_value(value, column = nil)
|
||||
self.class.connection.quote(value, column)
|
||||
end
|
||||
end
|
||||
end
|
147
activerecord/lib/active_record/scoping.rb
Normal file
147
activerecord/lib/active_record/scoping.rb
Normal file
|
@ -0,0 +1,147 @@
|
|||
require 'active_support/concern'
|
||||
|
||||
module ActiveRecord
|
||||
module Scoping
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# with_scope lets you apply options to inner block incrementally. It takes a hash and the keys must be
|
||||
# <tt>:find</tt> or <tt>:create</tt>. <tt>:find</tt> parameter is <tt>Relation</tt> while
|
||||
# <tt>:create</tt> parameters are an attributes hash.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.create_with_scope
|
||||
# with_scope(:find => where(:blog_id => 1), :create => { :blog_id => 1 }) do
|
||||
# find(1) # => SELECT * from articles WHERE blog_id = 1 AND id = 1
|
||||
# a = create(1)
|
||||
# a.blog_id # => 1
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In nested scopings, all previous parameters are overwritten by the innermost rule, with the exception of
|
||||
# <tt>where</tt>, <tt>includes</tt>, and <tt>joins</tt> operations in <tt>Relation</tt>, which are merged.
|
||||
#
|
||||
# <tt>joins</tt> operations are uniqued so multiple scopes can join in the same table without table aliasing
|
||||
# problems. If you need to join multiple tables, but still want one of the tables to be uniqued, use the
|
||||
# array of strings format for your joins.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.find_with_scope
|
||||
# with_scope(:find => where(:blog_id => 1).limit(1), :create => { :blog_id => 1 }) do
|
||||
# with_scope(:find => limit(10)) do
|
||||
# all # => SELECT * from articles WHERE blog_id = 1 LIMIT 10
|
||||
# end
|
||||
# with_scope(:find => where(:author_id => 3)) do
|
||||
# all # => SELECT * from articles WHERE blog_id = 1 AND author_id = 3 LIMIT 1
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can ignore any previous scopings by using the <tt>with_exclusive_scope</tt> method.
|
||||
#
|
||||
# class Article < ActiveRecord::Base
|
||||
# def self.find_with_exclusive_scope
|
||||
# with_scope(:find => where(:blog_id => 1).limit(1)) do
|
||||
# with_exclusive_scope(:find => limit(10)) do
|
||||
# all # => SELECT * from articles LIMIT 10
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
|
||||
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 scope.is_a?(Hash)
|
||||
# Dup first and second level of hash (method and params).
|
||||
scope = scope.dup
|
||||
scope.each do |method, params|
|
||||
scope[method] = params.dup unless params == true
|
||||
end
|
||||
|
||||
scope.assert_valid_keys([ :find, :create ])
|
||||
relation = construct_finder_arel(scope[:find] || {})
|
||||
relation.default_scoped = true unless action == :overwrite
|
||||
|
||||
if previous_scope && previous_scope.create_with_value && scope[:create]
|
||||
scope_for_create = if action == :merge
|
||||
previous_scope.create_with_value.merge(scope[:create])
|
||||
else
|
||||
scope[:create]
|
||||
end
|
||||
|
||||
relation = relation.create_with(scope_for_create)
|
||||
else
|
||||
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
|
||||
|
||||
scope = relation
|
||||
end
|
||||
|
||||
scope = previous_scope.merge(scope) if previous_scope && action == :merge
|
||||
|
||||
self.current_scope = scope
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
self.current_scope = previous_scope
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Works like with_scope, but discards any nested properties.
|
||||
def with_exclusive_scope(method_scoping = {}, &block)
|
||||
if method_scoping.values.any? { |e| e.is_a?(ActiveRecord::Relation) }
|
||||
raise ArgumentError, <<-MSG
|
||||
New finder API can not be used with_exclusive_scope. You can either call unscoped to get an anonymous scope not bound to the default_scope:
|
||||
|
||||
User.unscoped.where(:active => true)
|
||||
|
||||
Or call unscoped with a block:
|
||||
|
||||
User.unscoped do
|
||||
User.where(:active => true).all
|
||||
end
|
||||
|
||||
MSG
|
||||
end
|
||||
with_scope(method_scoping, :overwrite, &block)
|
||||
end
|
||||
|
||||
def current_scope #:nodoc:
|
||||
Thread.current["#{self}_current_scope"]
|
||||
end
|
||||
|
||||
def current_scope=(scope) #:nodoc:
|
||||
Thread.current["#{self}_current_scope"] = scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def construct_finder_arel(options = {}, scope = nil)
|
||||
relation = options.is_a?(Hash) ? unscoped.apply_finder_options(options) : options
|
||||
relation = scope.merge(relation) if scope
|
||||
relation
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def populate_with_current_scope_attributes
|
||||
return unless self.class.scope_attributes?
|
||||
|
||||
self.class.scope_attributes.each do |att,value|
|
||||
send("#{att}=", value) if respond_to?("#{att}=")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
22
activerecord/lib/active_record/translation.rb
Normal file
22
activerecord/lib/active_record/translation.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
module ActiveRecord
|
||||
module Translation
|
||||
include ActiveModel::Translation
|
||||
|
||||
# Set the lookup ancestors for ActiveModel.
|
||||
def lookup_ancestors #:nodoc:
|
||||
klass = self
|
||||
classes = [klass]
|
||||
return classes if klass == ActiveRecord::Base
|
||||
|
||||
while klass != klass.base_class
|
||||
classes << klass = klass.superclass
|
||||
end
|
||||
classes
|
||||
end
|
||||
|
||||
# Set the i18n scope to overwrite ActiveModel.
|
||||
def i18n_scope #:nodoc:
|
||||
:activerecord
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1196,19 +1196,6 @@ class BasicsTest < ActiveRecord::TestCase
|
|||
assert(auto.id > 0)
|
||||
end
|
||||
|
||||
def quote_column_name(name)
|
||||
"<#{name}>"
|
||||
end
|
||||
|
||||
def test_quote_keys
|
||||
ar = AutoId.new
|
||||
source = {"foo" => "bar", "baz" => "quux"}
|
||||
actual = ar.send(:quote_columns, self, source)
|
||||
inverted = actual.invert
|
||||
assert_equal("<foo>", inverted["bar"])
|
||||
assert_equal("<baz>", inverted["quux"])
|
||||
end
|
||||
|
||||
def test_sql_injection_via_find
|
||||
assert_raise(ActiveRecord::RecordNotFound, ActiveRecord::StatementInvalid) do
|
||||
Topic.find("123456 OR id > 0")
|
||||
|
|
Loading…
Reference in a new issue