Rails 2.1.1
Among other things, a security fix.
This commit is contained in:
parent
d2c4c8737c
commit
d4f97345db
354 changed files with 21027 additions and 3072 deletions
18
vendor/rails/activerecord/lib/active_record.rb
vendored
18
vendor/rails/activerecord/lib/active_record.rb
vendored
|
@ -24,16 +24,14 @@
|
|||
$:.unshift(File.dirname(__FILE__)) unless
|
||||
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
||||
|
||||
unless defined? ActiveSupport
|
||||
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
|
||||
if File.exist?(active_support_path)
|
||||
$:.unshift active_support_path
|
||||
require 'active_support'
|
||||
else
|
||||
require 'rubygems'
|
||||
gem 'activesupport'
|
||||
require 'active_support'
|
||||
end
|
||||
active_support_path = File.dirname(__FILE__) + "/../../activesupport/lib"
|
||||
if File.exist?(active_support_path)
|
||||
$:.unshift active_support_path
|
||||
require 'active_support'
|
||||
else
|
||||
require 'rubygems'
|
||||
gem 'activesupport'
|
||||
require 'active_support'
|
||||
end
|
||||
|
||||
require 'active_record/base'
|
||||
|
|
|
@ -51,9 +51,7 @@ module ActiveRecord
|
|||
|
||||
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
|
||||
parent_records.each do |parent_record|
|
||||
association_proxy = parent_record.send(reflection_name)
|
||||
association_proxy.loaded
|
||||
association_proxy.target = associated_record
|
||||
parent_record.send("set_#{reflection_name}_target", associated_record)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -103,17 +101,17 @@ module ActiveRecord
|
|||
associated_records = reflection.klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:joins => "INNER JOIN #{connection.quote_table_name options[:join_table]} as t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as _parent_record_id",
|
||||
:select => "#{options[:select] || table_name+'.*'}, t0.#{reflection.primary_key_name} as the_parent_record_id",
|
||||
:order => options[:order])
|
||||
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, '_parent_record_id')
|
||||
set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'the_parent_record_id')
|
||||
end
|
||||
|
||||
def preload_has_one_association(records, reflection, preload_options={})
|
||||
id_to_record_map, ids = construct_id_map(records)
|
||||
options = reflection.options
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
if options[:through]
|
||||
records.each {|record| record.send(reflection.name) && record.send(reflection.name).loaded}
|
||||
through_records = preload_through_records(records, reflection, options[:through])
|
||||
through_reflection = reflections[options[:through]]
|
||||
through_primary_key = through_reflection.primary_key_name
|
||||
|
@ -126,8 +124,6 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
else
|
||||
records.each {|record| record.send("set_#{reflection.name}_target", nil)}
|
||||
|
||||
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(ids, reflection, preload_options), reflection.primary_key_name)
|
||||
end
|
||||
end
|
||||
|
@ -188,7 +184,6 @@ module ActiveRecord
|
|||
through_records
|
||||
end
|
||||
|
||||
# FIXME: quoting
|
||||
def preload_belongs_to_association(records, reflection, preload_options={})
|
||||
options = reflection.options
|
||||
primary_key_name = reflection.primary_key_name
|
||||
|
@ -227,9 +222,19 @@ module ActiveRecord
|
|||
|
||||
table_name = klass.quoted_table_name
|
||||
primary_key = klass.primary_key
|
||||
conditions = "#{table_name}.#{primary_key} IN (?)"
|
||||
conditions = "#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)"
|
||||
conditions << append_conditions(options, preload_options)
|
||||
associated_records = klass.find(:all, :conditions => [conditions, id_map.keys.uniq],
|
||||
column_type = klass.columns.detect{|c| c.name == primary_key}.type
|
||||
ids = id_map.keys.uniq.map do |id|
|
||||
if column_type == :integer
|
||||
id.to_i
|
||||
elsif column_type == :float
|
||||
id.to_f
|
||||
else
|
||||
id
|
||||
end
|
||||
end
|
||||
associated_records = klass.find(:all, :conditions => [conditions, ids],
|
||||
:include => options[:include],
|
||||
:select => options[:select],
|
||||
:joins => options[:joins],
|
||||
|
@ -243,7 +248,7 @@ module ActiveRecord
|
|||
table_name = reflection.klass.quoted_table_name
|
||||
|
||||
if interface = reflection.options[:as]
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.name.demodulize}'"
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_id"} IN (?) and #{reflection.klass.quoted_table_name}.#{connection.quote_column_name "#{interface}_type"} = '#{self.base_class.sti_name}'"
|
||||
else
|
||||
foreign_key = reflection.primary_key_name
|
||||
conditions = "#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)"
|
||||
|
|
|
@ -690,6 +690,7 @@ module ActiveRecord
|
|||
# association is a polymorphic +belongs_to+.
|
||||
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
||||
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
||||
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. true by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_many :comments, :order => "posted_on"
|
||||
|
@ -710,6 +711,7 @@ module ActiveRecord
|
|||
|
||||
configure_dependency_for_has_many(reflection)
|
||||
|
||||
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
|
||||
add_multiple_associated_save_callbacks(reflection.name)
|
||||
add_association_callbacks(reflection.name, reflection.options)
|
||||
|
||||
|
@ -769,6 +771,7 @@ module ActiveRecord
|
|||
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
|
||||
# association is a polymorphic +belongs_to+.
|
||||
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
||||
# * <tt>:validate</tt> - If false, don't validate the associated object when saving the parent object. +false+ by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_one :credit_card, :dependent => :destroy # destroys the associated credit card
|
||||
|
@ -799,7 +802,7 @@ module ActiveRecord
|
|||
end
|
||||
after_save method_name
|
||||
|
||||
add_single_associated_save_callbacks(reflection.name)
|
||||
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
||||
association_accessor_methods(reflection, HasOneAssociation)
|
||||
association_constructor_method(:build, reflection, HasOneAssociation)
|
||||
association_constructor_method(:create, reflection, HasOneAssociation)
|
||||
|
@ -857,6 +860,7 @@ module ActiveRecord
|
|||
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
||||
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
||||
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
||||
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
|
||||
#
|
||||
# Option examples:
|
||||
# belongs_to :firm, :foreign_key => "client_of"
|
||||
|
@ -937,6 +941,8 @@ module ActiveRecord
|
|||
)
|
||||
end
|
||||
|
||||
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
||||
|
||||
configure_dependency_for_belongs_to(reflection)
|
||||
end
|
||||
|
||||
|
@ -1025,6 +1031,7 @@ module ActiveRecord
|
|||
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
||||
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
||||
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
||||
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +true+ by default.
|
||||
#
|
||||
# Option examples:
|
||||
# has_and_belongs_to_many :projects
|
||||
|
@ -1037,6 +1044,7 @@ module ActiveRecord
|
|||
def has_and_belongs_to_many(association_id, options = {}, &extension)
|
||||
reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
|
||||
|
||||
add_multiple_associated_validation_callbacks(reflection.name) unless options[:validate] == false
|
||||
add_multiple_associated_save_callbacks(reflection.name)
|
||||
collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
|
||||
|
||||
|
@ -1103,10 +1111,9 @@ module ActiveRecord
|
|||
association.create_through_record(new_value)
|
||||
self.send(reflection.name, new_value)
|
||||
else
|
||||
association.replace(new_value)
|
||||
association.replace(new_value)
|
||||
instance_variable_set(ivar, new_value.nil? ? nil : association)
|
||||
end
|
||||
|
||||
instance_variable_set(ivar, new_value.nil? ? nil : association)
|
||||
end
|
||||
|
||||
define_method("set_#{reflection.name}_target") do |target|
|
||||
|
@ -1157,7 +1164,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def add_single_associated_save_callbacks(association_name)
|
||||
def add_single_associated_validation_callbacks(association_name)
|
||||
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
||||
define_method(method_name) do
|
||||
association = instance_variable_get("@#{association_name}")
|
||||
|
@ -1169,7 +1176,7 @@ module ActiveRecord
|
|||
validate method_name
|
||||
end
|
||||
|
||||
def add_multiple_associated_save_callbacks(association_name)
|
||||
def add_multiple_associated_validation_callbacks(association_name)
|
||||
method_name = "validate_associated_records_for_#{association_name}".to_sym
|
||||
ivar = "@#{association_name}"
|
||||
|
||||
|
@ -1190,6 +1197,10 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
validate method_name
|
||||
end
|
||||
|
||||
def add_multiple_associated_save_callbacks(association_name)
|
||||
ivar = "@#{association_name}"
|
||||
|
||||
method_name = "before_save_associated_records_for_#{association_name}".to_sym
|
||||
define_method(method_name) do
|
||||
|
@ -1211,7 +1222,6 @@ module ActiveRecord
|
|||
else
|
||||
[]
|
||||
end
|
||||
|
||||
records_to_save.each { |record| association.send(:insert_record, record) } unless records_to_save.blank?
|
||||
|
||||
# reconstruct the SQL queries now that we know the owner's id
|
||||
|
@ -1343,7 +1353,8 @@ module ActiveRecord
|
|||
:uniq,
|
||||
:finder_sql, :counter_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
)
|
||||
|
||||
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
||||
|
@ -1353,7 +1364,7 @@ module ActiveRecord
|
|||
|
||||
def create_has_one_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :readonly, :validate
|
||||
)
|
||||
|
||||
create_reflection(:has_one, association_id, options, self)
|
||||
|
@ -1361,7 +1372,7 @@ module ActiveRecord
|
|||
|
||||
def create_has_one_through_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type
|
||||
:class_name, :foreign_key, :remote, :select, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as, :through, :source, :source_type, :validate
|
||||
)
|
||||
create_reflection(:has_one, association_id, options, self)
|
||||
end
|
||||
|
@ -1369,7 +1380,7 @@ module ActiveRecord
|
|||
def create_belongs_to_reflection(association_id, options)
|
||||
options.assert_valid_keys(
|
||||
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
|
||||
:counter_cache, :extend, :polymorphic, :readonly
|
||||
:counter_cache, :extend, :polymorphic, :readonly, :validate
|
||||
)
|
||||
|
||||
reflection = create_reflection(:belongs_to, association_id, options, self)
|
||||
|
@ -1388,7 +1399,8 @@ module ActiveRecord
|
|||
:uniq,
|
||||
:finder_sql, :delete_sql, :insert_sql,
|
||||
:before_add, :after_add, :before_remove, :after_remove,
|
||||
:extend, :readonly
|
||||
:extend, :readonly,
|
||||
:validate
|
||||
)
|
||||
|
||||
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
||||
|
@ -1465,10 +1477,15 @@ module ActiveRecord
|
|||
join_dependency.joins_for_table_name(table)
|
||||
}.flatten.compact.uniq
|
||||
|
||||
order = options[:order]
|
||||
if scoped_order = (scope && scope[:order])
|
||||
order = order ? "#{order}, #{scoped_order}" : scoped_order
|
||||
end
|
||||
|
||||
is_distinct = !options[:joins].blank? || include_eager_conditions?(options, tables_from_conditions) || include_eager_order?(options, tables_from_order)
|
||||
sql = "SELECT "
|
||||
if is_distinct
|
||||
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", options[:order])
|
||||
sql << connection.distinct("#{connection.quote_table_name table_name}.#{primary_key}", order)
|
||||
else
|
||||
sql << primary_key
|
||||
end
|
||||
|
@ -1482,8 +1499,8 @@ module ActiveRecord
|
|||
add_conditions!(sql, options[:conditions], scope)
|
||||
add_group!(sql, options[:group], scope)
|
||||
|
||||
if options[:order] && is_distinct
|
||||
connection.add_order_by_for_association_limiting!(sql, options)
|
||||
if order && is_distinct
|
||||
connection.add_order_by_for_association_limiting!(sql, :order => order)
|
||||
else
|
||||
add_order!(sql, options[:order], scope)
|
||||
end
|
||||
|
@ -1502,19 +1519,19 @@ module ActiveRecord
|
|||
else all << cond
|
||||
end
|
||||
end
|
||||
conditions.join(' ').scan(/([\.\w]+).?\./).flatten
|
||||
conditions.join(' ').scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
end
|
||||
|
||||
def order_tables(options)
|
||||
order = options[:order]
|
||||
order = [options[:order], scope(:find, :order) ].join(", ")
|
||||
return [] unless order && order.is_a?(String)
|
||||
order.scan(/([\.\w]+).?\./).flatten
|
||||
order.scan(/([\.a-zA-Z_]+).?\./).flatten
|
||||
end
|
||||
|
||||
def selects_tables(options)
|
||||
select = options[:select]
|
||||
return [] unless select && select.is_a?(String)
|
||||
select.scan(/"?([\.\w]+)"?.?\./).flatten
|
||||
select.scan(/"?([\.a-zA-Z_]+)"?.?\./).flatten
|
||||
end
|
||||
|
||||
# Checks if the conditions reference a table other than the current model table
|
||||
|
@ -1638,7 +1655,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def join_for_table_name(table_name)
|
||||
@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
|
||||
join = (@joins.select{|j|j.aliased_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first) rescue nil
|
||||
return join unless join.nil?
|
||||
@joins.select{|j|j.is_a?(JoinAssociation) && j.aliased_join_table_name == table_name.gsub(/^\"(.*)\"$/){$1} }.first rescue nil
|
||||
end
|
||||
|
||||
def joins_for_table_name(table_name)
|
||||
|
@ -1714,6 +1733,7 @@ module ActiveRecord
|
|||
collection.target.push(association)
|
||||
when :has_one
|
||||
return if record.id.to_s != join.parent.record_id(row).to_s
|
||||
return if record.instance_variable_defined?("@#{join.reflection.name}")
|
||||
association = join.instantiate(row) unless row[join.aliased_primary_key].nil?
|
||||
record.send("set_#{join.reflection.name}_target", association)
|
||||
when :belongs_to
|
||||
|
@ -1795,7 +1815,7 @@ module ActiveRecord
|
|||
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
|
||||
end
|
||||
|
||||
if reflection.macro == :has_many && reflection.options[:through]
|
||||
if [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
|
||||
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
|
||||
end
|
||||
end
|
||||
|
@ -1819,7 +1839,7 @@ module ActiveRecord
|
|||
]
|
||||
when :has_many, :has_one
|
||||
case
|
||||
when reflection.macro == :has_many && reflection.options[:through]
|
||||
when reflection.options[:through]
|
||||
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
|
||||
|
||||
jt_foreign_key = jt_as_extra = jt_source_extra = jt_sti_extra = nil
|
||||
|
@ -1855,7 +1875,7 @@ module ActiveRecord
|
|||
jt_sti_extra = " AND %s.%s = %s" % [
|
||||
connection.quote_table_name(aliased_join_table_name),
|
||||
connection.quote_column_name(through_reflection.active_record.inheritance_column),
|
||||
through_reflection.klass.quote_value(through_reflection.klass.name.demodulize)]
|
||||
through_reflection.klass.quote_value(through_reflection.klass.sti_name)]
|
||||
end
|
||||
when :belongs_to
|
||||
first_key = primary_key
|
||||
|
@ -1920,10 +1940,8 @@ module ActiveRecord
|
|||
else
|
||||
""
|
||||
end || ''
|
||||
join << %(AND %s.%s = %s ) % [
|
||||
connection.quote_table_name(aliased_table_name),
|
||||
connection.quote_column_name(klass.inheritance_column),
|
||||
klass.quote_value(klass.name.demodulize)] unless klass.descends_from_active_record?
|
||||
join << %(AND %s) % [
|
||||
klass.send(:type_condition, aliased_table_name)] unless klass.descends_from_active_record?
|
||||
|
||||
[through_reflection, reflection].each do |ref|
|
||||
join << "AND #{interpolate_sql(sanitize_sql(ref.options[:conditions]))} " if ref && ref.options[:conditions]
|
||||
|
|
|
@ -78,11 +78,14 @@ module ActiveRecord
|
|||
@loaded = false
|
||||
end
|
||||
|
||||
def build(attributes = {})
|
||||
def build(attributes = {}, &block)
|
||||
if attributes.is_a?(Array)
|
||||
attributes.collect { |attr| build(attr) }
|
||||
attributes.collect { |attr| build(attr, &block) }
|
||||
else
|
||||
build_record(attributes) { |record| set_belongs_to_association_for(record) }
|
||||
build_record(attributes) do |record|
|
||||
block.call(record) if block_given?
|
||||
set_belongs_to_association_for(record)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -187,7 +190,7 @@ module ActiveRecord
|
|||
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
||||
@target.size
|
||||
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
||||
unsaved_records = Array(@target.detect { |r| r.new_record? })
|
||||
unsaved_records = @target.select { |r| r.new_record? }
|
||||
unsaved_records.size + count_records
|
||||
else
|
||||
count_records
|
||||
|
@ -335,7 +338,7 @@ module ActiveRecord
|
|||
callback(:before_add, record)
|
||||
yield(record) if block_given?
|
||||
@target ||= [] unless loaded?
|
||||
@target << record
|
||||
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
||||
callback(:after_add, record)
|
||||
record
|
||||
end
|
||||
|
|
|
@ -69,8 +69,8 @@ module ActiveRecord
|
|||
@target
|
||||
end
|
||||
|
||||
def respond_to?(symbol, include_priv = false)
|
||||
proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
|
||||
def respond_to?(*args)
|
||||
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
||||
end
|
||||
|
||||
# Explicitly proxy === because the instance method removal above
|
||||
|
@ -131,10 +131,6 @@ module ActiveRecord
|
|||
records.map { |record| record.quoted_id }.join(',')
|
||||
end
|
||||
|
||||
def interpolate_sql_options!(options, *keys)
|
||||
keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
|
||||
end
|
||||
|
||||
def interpolate_sql(sql, record = nil)
|
||||
@owner.send(:interpolate_sql, sql, record)
|
||||
end
|
||||
|
|
|
@ -70,10 +70,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def construct_sql
|
||||
interpolate_sql_options!(@reflection.options, :finder_sql)
|
||||
|
||||
if @reflection.options[:finder_sql]
|
||||
@finder_sql = @reflection.options[:finder_sql]
|
||||
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
||||
else
|
||||
@finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
|
||||
@finder_sql << " AND (#{conditions})" if conditions
|
||||
|
@ -87,6 +85,7 @@ module ActiveRecord
|
|||
:joins => @join_sql,
|
||||
:readonly => false,
|
||||
:order => @reflection.options[:order],
|
||||
:include => @reflection.options[:include],
|
||||
:limit => @reflection.options[:limit] } }
|
||||
end
|
||||
|
||||
|
|
|
@ -14,7 +14,16 @@ module ActiveRecord
|
|||
@finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
|
||||
options[:include] ||= @reflection.options[:include]
|
||||
|
||||
@reflection.klass.count(column_name, options)
|
||||
value = @reflection.klass.count(column_name, options)
|
||||
|
||||
limit = @reflection.options[:limit]
|
||||
offset = @reflection.options[:offset]
|
||||
|
||||
if limit || offset
|
||||
[ [value - offset.to_i, 0].max, limit.to_i ].min
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,8 +36,11 @@ module ActiveRecord
|
|||
else
|
||||
@reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
|
||||
end
|
||||
|
||||
@target = [] and loaded if count == 0
|
||||
|
||||
# If there's nothing in the database and @target has no new records
|
||||
# we are certain the current target is an empty array. This is a
|
||||
# documented side-effect of the method that may avoid an extra SELECT.
|
||||
@target ||= [] and loaded if count == 0
|
||||
|
||||
if @reflection.options[:limit]
|
||||
count = [ @reflection.options[:limit], count ].min
|
||||
|
@ -100,7 +112,7 @@ module ActiveRecord
|
|||
create_scoping = {}
|
||||
set_belongs_to_association_for(create_scoping)
|
||||
{
|
||||
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] },
|
||||
:find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit], :include => @reflection.options[:include]},
|
||||
:create => create_scoping
|
||||
}
|
||||
end
|
||||
|
|
|
@ -237,7 +237,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def build_sti_condition
|
||||
"#{@reflection.through_reflection.quoted_table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.sti_name)}"
|
||||
@reflection.through_reflection.klass.send(:type_condition)
|
||||
end
|
||||
|
||||
alias_method :sql_conditions, :conditions
|
||||
|
|
|
@ -21,8 +21,8 @@ module ActiveRecord
|
|||
def replace(obj, dont_save = false)
|
||||
load_target
|
||||
|
||||
unless @target.nil?
|
||||
if dependent? && !dont_save && @target != obj
|
||||
unless @target.nil? || @target == obj
|
||||
if dependent? && !dont_save
|
||||
@target.destroy unless @target.new_record?
|
||||
@owner.clear_association_cache
|
||||
else
|
||||
|
|
|
@ -22,6 +22,10 @@ module ActiveRecord
|
|||
|
||||
def find_target
|
||||
super.first
|
||||
end
|
||||
|
||||
def reset_target!
|
||||
@target = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -372,7 +372,7 @@ module ActiveRecord #:nodoc:
|
|||
def self.reset_subclasses #:nodoc:
|
||||
nonreloadables = []
|
||||
subclasses.each do |klass|
|
||||
unless Dependencies.autoloaded? klass
|
||||
unless ActiveSupport::Dependencies.autoloaded? klass
|
||||
nonreloadables << klass
|
||||
next
|
||||
end
|
||||
|
@ -439,6 +439,10 @@ module ActiveRecord #:nodoc:
|
|||
cattr_accessor :schema_format , :instance_writer => false
|
||||
@@schema_format = :ruby
|
||||
|
||||
# Specify whether or not to use timestamps for migration numbers
|
||||
cattr_accessor :timestamped_migrations , :instance_writer => false
|
||||
@@timestamped_migrations = true
|
||||
|
||||
# Determine whether to store the full constant name including namespace when using STI
|
||||
superclass_delegating_accessor :store_full_sti_class
|
||||
self.store_full_sti_class = false
|
||||
|
@ -828,7 +832,7 @@ module ActiveRecord #:nodoc:
|
|||
def update_counters(id, counters)
|
||||
updates = counters.inject([]) { |list, (counter_name, increment)|
|
||||
sign = increment < 0 ? "-" : "+"
|
||||
list << "#{connection.quote_column_name(counter_name)} = #{connection.quote_column_name(counter_name)} #{sign} #{increment.abs}"
|
||||
list << "#{connection.quote_column_name(counter_name)} = COALESCE(#{connection.quote_column_name(counter_name)}, 0) #{sign} #{increment.abs}"
|
||||
}.join(", ")
|
||||
update_all(updates, "#{connection.quote_column_name(primary_key)} = #{quote_value(id)}")
|
||||
end
|
||||
|
@ -1465,7 +1469,7 @@ module ActiveRecord #:nodoc:
|
|||
|
||||
def construct_finder_sql(options)
|
||||
scope = scope(:find)
|
||||
sql = "SELECT #{options[:select] || (scope && scope[:select]) || (options[:joins] && quoted_table_name + '.*') || '*'} "
|
||||
sql = "SELECT #{options[:select] || (scope && scope[:select]) || ((options[:joins] || (scope && scope[:joins])) && quoted_table_name + '.*') || '*'} "
|
||||
sql << "FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
|
||||
|
||||
add_joins!(sql, options, scope)
|
||||
|
@ -1577,10 +1581,11 @@ module ActiveRecord #:nodoc:
|
|||
sql << "WHERE #{merged_conditions} " unless merged_conditions.blank?
|
||||
end
|
||||
|
||||
def type_condition
|
||||
def type_condition(table_alias=nil)
|
||||
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
|
||||
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
|
||||
type_condition = subclasses.inject("#{quoted_table_name}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
|
||||
condition << "OR #{quoted_table_name}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
|
||||
type_condition = subclasses.inject("#{quoted_table_alias}.#{quoted_inheritance_column} = '#{sti_name}' ") do |condition, subclass|
|
||||
condition << "OR #{quoted_table_alias}.#{quoted_inheritance_column} = '#{subclass.sti_name}' "
|
||||
end
|
||||
|
||||
" (#{type_condition}) "
|
||||
|
@ -1717,7 +1722,7 @@ module ActiveRecord #:nodoc:
|
|||
def attribute_condition(argument)
|
||||
case argument
|
||||
when nil then "IS ?"
|
||||
when Array, ActiveRecord::Associations::AssociationCollection then "IN (?)"
|
||||
when Array, ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope then "IN (?)"
|
||||
when Range then "BETWEEN ? AND ?"
|
||||
else "= ?"
|
||||
end
|
||||
|
@ -2053,9 +2058,10 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
|
||||
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
|
||||
statement.gsub(/:([a-zA-Z]\w*)/) do
|
||||
match = $1.to_sym
|
||||
if bind_vars.include?(match)
|
||||
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}"
|
||||
|
@ -2064,13 +2070,18 @@ module ActiveRecord #:nodoc:
|
|||
end
|
||||
|
||||
def expand_range_bind_variables(bind_vars) #:nodoc:
|
||||
bind_vars.sum do |var|
|
||||
expanded = []
|
||||
|
||||
bind_vars.each do |var|
|
||||
if var.is_a?(Range)
|
||||
[var.first, var.last]
|
||||
expanded << var.first
|
||||
expanded << var.last
|
||||
else
|
||||
[var]
|
||||
expanded << var
|
||||
end
|
||||
end
|
||||
|
||||
expanded
|
||||
end
|
||||
|
||||
def quote_bound_value(value) #:nodoc:
|
||||
|
@ -2572,8 +2583,15 @@ module ActiveRecord #:nodoc:
|
|||
quoted = {}
|
||||
connection = self.class.connection
|
||||
attribute_names.each do |name|
|
||||
if column = column_for_attribute(name)
|
||||
quoted[name] = connection.quote(read_attribute(name), column) unless !include_primary_key && column.primary
|
||||
if (column = column_for_attribute(name)) && (include_primary_key || !column.primary)
|
||||
value = read_attribute(name)
|
||||
|
||||
# We need explicit to_yaml because quote() does not properly convert Time/Date fields to YAML.
|
||||
if value && self.class.serialized_attributes.has_key?(name) && (value.acts_like?(:date) || value.acts_like?(:time))
|
||||
value = value.to_yaml
|
||||
end
|
||||
|
||||
quoted[name] = connection.quote(value, column)
|
||||
end
|
||||
end
|
||||
include_readonly_attributes ? quoted : remove_readonly_attributes(quoted)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module ActiveRecord
|
||||
module Calculations #:nodoc:
|
||||
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include]
|
||||
CALCULATIONS_OPTIONS = [:conditions, :joins, :order, :select, :group, :having, :distinct, :limit, :offset, :include, :from]
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
@ -27,6 +27,8 @@ module ActiveRecord
|
|||
# * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
|
||||
# include the joined columns.
|
||||
# * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
|
||||
# * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
|
||||
# of a database view).
|
||||
#
|
||||
# Examples for counting all:
|
||||
# Person.count # returns the total count of all people
|
||||
|
@ -71,7 +73,7 @@ module ActiveRecord
|
|||
#
|
||||
# Person.sum('age')
|
||||
def sum(column_name, options = {})
|
||||
calculate(:sum, column_name, options) || 0
|
||||
calculate(:sum, column_name, options)
|
||||
end
|
||||
|
||||
# This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
|
||||
|
@ -178,13 +180,23 @@ module ActiveRecord
|
|||
sql = "SELECT COUNT(*) AS #{aggregate_alias}" if use_workaround
|
||||
|
||||
sql << ", #{options[:group_field]} AS #{options[:group_alias]}" if options[:group]
|
||||
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
|
||||
sql << " FROM #{connection.quote_table_name(table_name)} "
|
||||
if options[:from]
|
||||
sql << " FROM #{options[:from]} "
|
||||
else
|
||||
sql << " FROM (SELECT #{distinct}#{column_name}" if use_workaround
|
||||
sql << " FROM #{connection.quote_table_name(table_name)} "
|
||||
end
|
||||
|
||||
joins = ""
|
||||
add_joins!(joins, options, scope)
|
||||
|
||||
if merged_includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, options[:joins])
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, joins)
|
||||
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
||||
end
|
||||
add_joins!(sql, options, scope)
|
||||
|
||||
sql << joins unless joins.blank?
|
||||
|
||||
add_conditions!(sql, options[:conditions], scope)
|
||||
add_limited_ids_condition!(sql, options, join_dependency) if join_dependency && !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
|
||||
|
||||
|
@ -205,7 +217,7 @@ module ActiveRecord
|
|||
|
||||
sql << " ORDER BY #{options[:order]} " if options[:order]
|
||||
add_limit!(sql, options, scope)
|
||||
sql << ')' if use_workaround
|
||||
sql << ') AS #{aggregate_alias}_subquery' if use_workaround
|
||||
sql
|
||||
end
|
||||
|
||||
|
@ -266,6 +278,7 @@ module ActiveRecord
|
|||
operation = operation.to_s.downcase
|
||||
case operation
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then value =~ /\./ ? value.to_f : value.to_i
|
||||
when 'avg' then value && value.to_f
|
||||
else column ? column.type_cast(value) : value
|
||||
end
|
||||
|
|
|
@ -257,7 +257,10 @@ module ActiveRecord
|
|||
|
||||
def to_sql
|
||||
column_sql = "#{base.quote_column_name(name)} #{sql_type}"
|
||||
add_column_options!(column_sql, :null => null, :default => default) unless type.to_sym == :primary_key
|
||||
column_options = {}
|
||||
column_options[:null] = null unless null.nil?
|
||||
column_options[:default] = default unless default.nil?
|
||||
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
|
||||
column_sql
|
||||
end
|
||||
alias to_s :to_sql
|
||||
|
@ -304,8 +307,7 @@ module ActiveRecord
|
|||
#
|
||||
# Available options are (none of these exists by default):
|
||||
# * <tt>:limit</tt> -
|
||||
# Requests a maximum column length (<tt>:string</tt>, <tt>:text</tt>,
|
||||
# <tt>:binary</tt> or <tt>:integer</tt> columns only)
|
||||
# Requests a maximum column length. This is number of characters for <tt>:string</tt> and <tt>:text</tt> columns and number of bytes for :binary and :integer columns.
|
||||
# * <tt>:default</tt> -
|
||||
# The column's default value. Use nil for NULL.
|
||||
# * <tt>:null</tt> -
|
||||
|
@ -442,9 +444,10 @@ module ActiveRecord
|
|||
|
||||
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
|
||||
# <tt>:updated_at</tt> to the table.
|
||||
def timestamps
|
||||
column(:created_at, :datetime)
|
||||
column(:updated_at, :datetime)
|
||||
def timestamps(*args)
|
||||
options = args.extract_options!
|
||||
column(:created_at, :datetime, options)
|
||||
column(:updated_at, :datetime, options)
|
||||
end
|
||||
|
||||
def references(*args)
|
||||
|
|
|
@ -331,15 +331,26 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def assume_migrated_upto_version(version)
|
||||
version = version.to_i
|
||||
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
|
||||
|
||||
migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
|
||||
versions = Dir['db/migrate/[0-9]*_*.rb'].map do |filename|
|
||||
filename.split('/').last.split('_').first.to_i
|
||||
end
|
||||
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" unless migrated.include?(version.to_i)
|
||||
(versions - migrated).select { |v| v < version.to_i }.each do |v|
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
|
||||
unless migrated.include?(version)
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')"
|
||||
end
|
||||
|
||||
inserted = Set.new
|
||||
(versions - migrated).each do |v|
|
||||
if inserted.include?(v)
|
||||
raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict."
|
||||
elsif v < version
|
||||
execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')"
|
||||
inserted << v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -372,13 +383,9 @@ module ActiveRecord
|
|||
|
||||
def add_column_options!(sql, options) #:nodoc:
|
||||
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
|
||||
# must explcitly check for :null to allow change_column to work on migrations
|
||||
if options.has_key? :null
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
else
|
||||
sql << " NULL"
|
||||
end
|
||||
# must explicitly check for :null to allow change_column to work on migrations
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -50,10 +50,7 @@ module ActiveRecord
|
|||
rescue LoadError => cannot_require_mysql
|
||||
# Use the bundled Ruby/MySQL driver if no driver is already in place
|
||||
begin
|
||||
ActiveRecord::Base.logger.info(
|
||||
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
|
||||
"Please install the C-based MySQL library instead (gem install mysql)."
|
||||
) if ActiveRecord::Base.logger
|
||||
ActiveSupport::Deprecation.warn "You're using the Ruby-based MySQL library that ships with Rails. This library will be REMOVED FROM RAILS 2.2. Please switch to the offical mysql gem: `gem install mysql`", caller
|
||||
|
||||
require 'active_record/vendor/mysql'
|
||||
rescue LoadError
|
||||
|
@ -113,7 +110,8 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def extract_limit(sql_type)
|
||||
if sql_type =~ /blob|text/i
|
||||
case sql_type
|
||||
when /blob|text/i
|
||||
case sql_type
|
||||
when /tiny/i
|
||||
255
|
||||
|
@ -124,6 +122,11 @@ module ActiveRecord
|
|||
else
|
||||
super # we could return 65535 here, but we leave it undecorated by default
|
||||
end
|
||||
when /^bigint/i; 8
|
||||
when /^int/i; 4
|
||||
when /^mediumint/i; 3
|
||||
when /^smallint/i; 2
|
||||
when /^tinyint/i; 1
|
||||
else
|
||||
super
|
||||
end
|
||||
|
@ -193,10 +196,10 @@ module ActiveRecord
|
|||
|
||||
def native_database_types #:nodoc:
|
||||
{
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
|
||||
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY".freeze,
|
||||
:string => { :name => "varchar", :limit => 255 },
|
||||
:text => { :name => "text" },
|
||||
:integer => { :name => "int"},
|
||||
:integer => { :name => "int", :limit => 4 },
|
||||
:float => { :name => "float" },
|
||||
:decimal => { :name => "decimal" },
|
||||
:datetime => { :name => "datetime" },
|
||||
|
@ -336,10 +339,11 @@ module ActiveRecord
|
|||
|
||||
def add_limit_offset!(sql, options) #:nodoc:
|
||||
if limit = options[:limit]
|
||||
limit = sanitize_limit(limit)
|
||||
unless offset = options[:offset]
|
||||
sql << " LIMIT #{limit}"
|
||||
else
|
||||
sql << " LIMIT #{offset}, #{limit}"
|
||||
sql << " LIMIT #{offset.to_i}, #{limit}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -439,18 +443,29 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def change_column_default(table_name, column_name, default) #:nodoc:
|
||||
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
||||
column = column_for(table_name, column_name)
|
||||
change_column table_name, column_name, column.sql_type, :default => default
|
||||
end
|
||||
|
||||
execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}")
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
unless null || default.nil?
|
||||
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
||||
end
|
||||
|
||||
change_column table_name, column_name, column.sql_type, :null => null
|
||||
end
|
||||
|
||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
column = column_for(table_name, column_name)
|
||||
|
||||
unless options_include_default?(options)
|
||||
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
options[:default] = column.default
|
||||
else
|
||||
raise "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
options[:default] = column.default
|
||||
end
|
||||
|
||||
unless options.has_key?(:null)
|
||||
options[:null] = column.null
|
||||
end
|
||||
|
||||
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
|
@ -459,8 +474,17 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
||||
options = {}
|
||||
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
options[:default] = column.default
|
||||
options[:null] = column.null
|
||||
else
|
||||
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
||||
execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
||||
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
||||
add_column_options!(rename_column_sql, options)
|
||||
execute(rename_column_sql)
|
||||
end
|
||||
|
||||
# Maps logical Rails types to MySQL-specific data types.
|
||||
|
@ -468,14 +492,12 @@ module ActiveRecord
|
|||
return super unless type.to_s == 'integer'
|
||||
|
||||
case limit
|
||||
when 0..3
|
||||
"smallint(#{limit})"
|
||||
when 4..8
|
||||
"int(#{limit})"
|
||||
when 9..20
|
||||
"bigint(#{limit})"
|
||||
else
|
||||
'int(11)'
|
||||
when 1; 'tinyint'
|
||||
when 2; 'smallint'
|
||||
when 3; 'mediumint'
|
||||
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
||||
when 5..8; 'bigint'
|
||||
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -525,6 +547,13 @@ module ActiveRecord
|
|||
def version
|
||||
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
||||
end
|
||||
|
||||
def column_for(table_name, column_name)
|
||||
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
raise "No such column: #{table_name}.#{column_name}"
|
||||
end
|
||||
column
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,8 +23,8 @@ module ActiveRecord
|
|||
config = config.symbolize_keys
|
||||
host = config[:host]
|
||||
port = config[:port] || 5432
|
||||
username = config[:username].to_s
|
||||
password = config[:password].to_s
|
||||
username = config[:username].to_s if config[:username]
|
||||
password = config[:password].to_s if config[:password]
|
||||
|
||||
if config.has_key?(:database)
|
||||
database = config[:database]
|
||||
|
@ -47,6 +47,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
private
|
||||
def extract_limit(sql_type)
|
||||
case sql_type
|
||||
when /^bigint/i; 8
|
||||
when /^smallint/i; 2
|
||||
else super
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts the scale from PostgreSQL-specific data types.
|
||||
def extract_scale(sql_type)
|
||||
# Money type has a fixed scale of 2.
|
||||
|
@ -174,8 +182,8 @@ module ActiveRecord
|
|||
def self.extract_value_from_default(default)
|
||||
case default
|
||||
# Numeric types
|
||||
when /\A-?\d+(\.\d*)?\z/
|
||||
default
|
||||
when /\A\(?(-?\d+(\.\d*)?\)?)\z/
|
||||
$1
|
||||
# Character types
|
||||
when /\A'(.*)'::(?:character varying|bpchar|text)\z/m
|
||||
$1
|
||||
|
@ -319,6 +327,10 @@ module ActiveRecord
|
|||
has_support
|
||||
end
|
||||
|
||||
def supports_insert_with_returning?
|
||||
postgresql_version >= 80200
|
||||
end
|
||||
|
||||
# Returns the configured supported identifier length supported by PostgreSQL,
|
||||
# or report the default of 63 on PostgreSQL 7.x.
|
||||
def table_alias_length
|
||||
|
@ -360,7 +372,7 @@ module ActiveRecord
|
|||
# There are some incorrectly compiled postgres drivers out there
|
||||
# that don't define PGconn.escape.
|
||||
self.class.instance_eval do
|
||||
undef_method(:quote_string)
|
||||
remove_method(:quote_string)
|
||||
end
|
||||
end
|
||||
quote_string(s)
|
||||
|
@ -411,8 +423,34 @@ module ActiveRecord
|
|||
|
||||
# Executes an INSERT query and returns the new record's ID
|
||||
def insert(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('"', '')
|
||||
super || pk && last_insert_id(table, sequence_name || default_sequence_name(table, pk))
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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(table, sequence_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# create a 2D array representing the result set
|
||||
|
@ -492,13 +530,13 @@ module ActiveRecord
|
|||
option_string = options.symbolize_keys.sum do |key, value|
|
||||
case key
|
||||
when :owner
|
||||
" OWNER = '#{value}'"
|
||||
" OWNER = \"#{value}\""
|
||||
when :template
|
||||
" TEMPLATE = #{value}"
|
||||
" TEMPLATE = \"#{value}\""
|
||||
when :encoding
|
||||
" ENCODING = '#{value}'"
|
||||
when :tablespace
|
||||
" TABLESPACE = #{value}"
|
||||
" TABLESPACE = \"#{value}\""
|
||||
when :connection_limit
|
||||
" CONNECTION LIMIT = #{value}"
|
||||
else
|
||||
|
@ -506,7 +544,7 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
execute "CREATE DATABASE #{name}#{option_string}"
|
||||
execute "CREATE DATABASE #{quote_table_name(name)}#{option_string}"
|
||||
end
|
||||
|
||||
# Drops a PostgreSQL database
|
||||
|
@ -514,7 +552,15 @@ module ActiveRecord
|
|||
# Example:
|
||||
# drop_database 'matt_development'
|
||||
def drop_database(name) #:nodoc:
|
||||
execute "DROP DATABASE IF EXISTS #{name}"
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
|
@ -676,7 +722,7 @@ module ActiveRecord
|
|||
|
||||
# Renames a table.
|
||||
def rename_table(name, new_name)
|
||||
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
||||
execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
|
||||
end
|
||||
|
||||
# Adds a new column to the named table.
|
||||
|
@ -698,7 +744,8 @@ module ActiveRecord
|
|||
|
||||
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
|
||||
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
|
||||
|
@ -743,15 +790,14 @@ module ActiveRecord
|
|||
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
||||
return super unless type.to_s == 'integer'
|
||||
|
||||
if limit.nil? || limit == 4
|
||||
'integer'
|
||||
elsif limit < 4
|
||||
'smallint'
|
||||
else
|
||||
'bigint'
|
||||
case limit
|
||||
when 1..2; 'smallint'
|
||||
when 3..4, nil; 'integer'
|
||||
when 5..8; 'bigint'
|
||||
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
||||
#
|
||||
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
|
||||
|
|
|
@ -238,6 +238,15 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def change_column_null(table_name, column_name, null, default = nil)
|
||||
unless null || default.nil?
|
||||
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
||||
end
|
||||
alter_table(table_name) do |definition|
|
||||
definition[column_name].null = null
|
||||
end
|
||||
end
|
||||
|
||||
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
||||
alter_table(table_name) do |definition|
|
||||
include_default = options_include_default?(options)
|
||||
|
@ -251,6 +260,9 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
||||
unless columns(table_name).detect{|c| c.name == column_name.to_s }
|
||||
raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
|
||||
end
|
||||
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
|
||||
end
|
||||
|
||||
|
|
|
@ -123,7 +123,10 @@ module ActiveRecord
|
|||
attr = attr.to_s
|
||||
|
||||
# The attribute already has an unsaved change.
|
||||
unless changed_attributes.include?(attr)
|
||||
if changed_attributes.include?(attr)
|
||||
old = changed_attributes[attr]
|
||||
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
||||
else
|
||||
old = clone_attribute_value(:read_attribute, attr)
|
||||
changed_attributes[attr] = old if field_changed?(attr, old, value)
|
||||
end
|
||||
|
@ -134,7 +137,9 @@ module ActiveRecord
|
|||
|
||||
def update_with_dirty
|
||||
if partial_updates?
|
||||
update_without_dirty(changed)
|
||||
# Serialized attributes should always be written in case they've been
|
||||
# changed in place.
|
||||
update_without_dirty(changed | self.class.serialized_attributes.keys)
|
||||
else
|
||||
update_without_dirty
|
||||
end
|
||||
|
@ -142,9 +147,11 @@ module ActiveRecord
|
|||
|
||||
def field_changed?(attr, old, value)
|
||||
if column = column_for_attribute(attr)
|
||||
if column.type == :integer && column.null && old.nil?
|
||||
if column.type == :integer && column.null && (old.nil? || old == 0)
|
||||
# For nullable integer columns, NULL gets stored in database for blank (i.e. '') values.
|
||||
# Hence we don't record it as a change if the value changes from nil to ''.
|
||||
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
||||
# be typecast back to 0 (''.to_i => 0)
|
||||
value = nil if value.blank?
|
||||
else
|
||||
value = column.type_cast(value)
|
||||
|
|
|
@ -68,6 +68,7 @@ module ActiveRecord
|
|||
|
||||
def update_with_lock(attribute_names = @attributes.keys) #:nodoc:
|
||||
return update_without_lock(attribute_names) unless locking_enabled?
|
||||
return 0 if attribute_names.empty?
|
||||
|
||||
lock_col = self.class.locking_column
|
||||
previous_value = send(lock_col).to_i
|
||||
|
|
|
@ -238,6 +238,22 @@ module ActiveRecord
|
|||
# lower than the current schema version: when migrating up, those
|
||||
# never-applied "interleaved" migrations will be automatically applied, and
|
||||
# when migrating down, never-applied "interleaved" migrations will be skipped.
|
||||
#
|
||||
# == Timestamped Migrations
|
||||
#
|
||||
# By default, Rails generates migrations that look like:
|
||||
#
|
||||
# 20080717013526_your_migration_name.rb
|
||||
#
|
||||
# The prefix is a generation timestamp (in UTC).
|
||||
#
|
||||
# If you'd prefer to use numeric prefixes, you can turn timestamped migrations
|
||||
# off by setting:
|
||||
#
|
||||
# config.active_record.timestamped_migrations = false
|
||||
#
|
||||
# In environment.rb.
|
||||
#
|
||||
class Migration
|
||||
@@verbose = true
|
||||
cattr_accessor :verbose
|
||||
|
@ -369,11 +385,17 @@ module ActiveRecord
|
|||
Base.table_name_prefix + 'schema_migrations' + Base.table_name_suffix
|
||||
end
|
||||
|
||||
def get_all_versions
|
||||
Base.connection.select_values("SELECT version FROM #{schema_migrations_table_name}").map(&:to_i).sort
|
||||
end
|
||||
|
||||
def current_version
|
||||
version = Base.connection.select_values(
|
||||
"SELECT version FROM #{schema_migrations_table_name}"
|
||||
).map(&:to_i).max rescue nil
|
||||
version || 0
|
||||
sm_table = schema_migrations_table_name
|
||||
if Base.connection.table_exists?(sm_table)
|
||||
get_all_versions.max || 0
|
||||
else
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
def proper_table_name(name)
|
||||
|
@ -389,7 +411,7 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def current_version
|
||||
self.class.current_version
|
||||
migrated.last || 0
|
||||
end
|
||||
|
||||
def current_migration
|
||||
|
@ -399,7 +421,10 @@ module ActiveRecord
|
|||
def run
|
||||
target = migrations.detect { |m| m.version == @target_version }
|
||||
raise UnknownMigrationVersionError.new(@target_version) if target.nil?
|
||||
target.migrate(@direction)
|
||||
unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
|
||||
target.migrate(@direction)
|
||||
record_version_state_after_migrating(target.version)
|
||||
end
|
||||
end
|
||||
|
||||
def migrate
|
||||
|
@ -470,17 +495,19 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def migrated
|
||||
sm_table = self.class.schema_migrations_table_name
|
||||
Base.connection.select_values("SELECT version FROM #{sm_table}").map(&:to_i).sort
|
||||
@migrated_versions ||= self.class.get_all_versions
|
||||
end
|
||||
|
||||
private
|
||||
def record_version_state_after_migrating(version)
|
||||
sm_table = self.class.schema_migrations_table_name
|
||||
|
||||
@migrated_versions ||= []
|
||||
if down?
|
||||
@migrated_versions.delete(version.to_i)
|
||||
Base.connection.update("DELETE FROM #{sm_table} WHERE version = '#{version}'")
|
||||
else
|
||||
@migrated_versions.push(version.to_i).sort!
|
||||
Base.connection.insert("INSERT INTO #{sm_table} (version) VALUES ('#{version}')")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,6 +82,7 @@ module ActiveRecord
|
|||
# expected_options = { :conditions => { :colored => 'red' } }
|
||||
# assert_equal expected_options, Shirt.colored('red').proxy_options
|
||||
def named_scope(name, options = {}, &block)
|
||||
name = name.to_sym
|
||||
scopes[name] = lambda do |parent_scope, *args|
|
||||
Scope.new(parent_scope, case options
|
||||
when Hash
|
||||
|
@ -102,7 +103,7 @@ module ActiveRecord
|
|||
attr_reader :proxy_scope, :proxy_options
|
||||
|
||||
[].methods.each do |m|
|
||||
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|find|count|sum|average|maximum|minimum|paginate|first|last|empty?)/
|
||||
unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
|
||||
delegate m, :to => :proxy_found
|
||||
end
|
||||
end
|
||||
|
@ -139,6 +140,10 @@ module ActiveRecord
|
|||
@found ? @found.empty? : count.zero?
|
||||
end
|
||||
|
||||
def respond_to?(method, include_private = false)
|
||||
super || @proxy_scope.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
protected
|
||||
def proxy_found
|
||||
@found || load_found
|
||||
|
|
|
@ -20,7 +20,7 @@ module ActiveRecord
|
|||
# ActiveRecord::Base.observers = Cacher, GarbageCollector
|
||||
#
|
||||
# Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
|
||||
# called during startup, and before each development request.
|
||||
# called during startup, and before each development request.
|
||||
def observers=(*observers)
|
||||
@observers = observers.flatten
|
||||
end
|
||||
|
@ -130,11 +130,11 @@ module ActiveRecord
|
|||
# Observers register themselves in the model class they observe, since it is the class that
|
||||
# notifies them of events when they occur. As a side-effect, when an observer is loaded its
|
||||
# corresponding model class is loaded.
|
||||
#
|
||||
#
|
||||
# Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
|
||||
# application initializers. Now observers are loaded after application initializers,
|
||||
# application initializers. Now observers are loaded after application initializers,
|
||||
# so observed models can make use of extensions.
|
||||
#
|
||||
#
|
||||
# If by any chance you are using observed models in the initialization you can still
|
||||
# load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
|
||||
# singletons and that call instantiates and registers them.
|
||||
|
@ -189,7 +189,9 @@ module ActiveRecord
|
|||
|
||||
def add_observer!(klass)
|
||||
klass.add_observer(self)
|
||||
klass.class_eval 'def after_find() end' unless klass.respond_to?(:after_find)
|
||||
if respond_to?(:after_find) && !klass.method_defined?(:after_find)
|
||||
klass.class_eval 'def after_find() end'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -22,11 +22,22 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
|
||||
def assert_queries(num = 1)
|
||||
$query_count = 0
|
||||
def assert_sql(*patterns_to_match)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
assert_equal num, $query_count, "#{$query_count} instead of #{num} queries were executed."
|
||||
failed_patterns = []
|
||||
patterns_to_match.each do |pattern|
|
||||
failed_patterns << pattern unless $queries_executed.any?{ |sql| pattern === sql }
|
||||
end
|
||||
assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found."
|
||||
end
|
||||
|
||||
def assert_queries(num = 1)
|
||||
$queries_executed = []
|
||||
yield
|
||||
ensure
|
||||
assert_equal num, $queries_executed.size, "#{$queries_executed.size} instead of #{num} queries were executed."
|
||||
end
|
||||
|
||||
def assert_no_queries(&block)
|
||||
|
|
|
@ -480,8 +480,9 @@ module ActiveRecord
|
|||
# validates_length_of :fax, :in => 7..32, :allow_nil => true
|
||||
# validates_length_of :phone, :in => 7..32, :allow_blank => true
|
||||
# validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
|
||||
# validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
|
||||
# validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
|
||||
# validates_length_of :fav_bra_size, :minimum => 1, :too_short => "please enter at least %d character"
|
||||
# validates_length_of :smurf_leader, :is => 4, :message => "papa is spelled with %d characters... don't play me."
|
||||
# validates_length_of :essay, :minimum => 100, :too_short => "Your essay must be at least %d words."), :tokenizer => lambda {|str| str.scan(/\w+/) }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
|
@ -492,7 +493,6 @@ module ActiveRecord
|
|||
# * <tt>:in</tt> - A synonym(or alias) for <tt>:within</tt>.
|
||||
# * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
|
||||
# * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
|
||||
#
|
||||
# * <tt>:too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (maximum is %d characters)").
|
||||
# * <tt>:too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)").
|
||||
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt> method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)").
|
||||
|
@ -504,12 +504,16 @@ module ActiveRecord
|
|||
# * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
|
||||
# count words as in above example.)
|
||||
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
|
||||
def validates_length_of(*attrs)
|
||||
# Merge given options with defaults.
|
||||
options = {
|
||||
:too_long => ActiveRecord::Errors.default_error_messages[:too_long],
|
||||
:too_short => ActiveRecord::Errors.default_error_messages[:too_short],
|
||||
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
|
||||
:wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length],
|
||||
:tokenizer => lambda {|value| value.split(//)}
|
||||
}.merge(DEFAULT_VALIDATION_OPTIONS)
|
||||
options.update(attrs.extract_options!.symbolize_keys)
|
||||
|
||||
|
@ -536,7 +540,7 @@ module ActiveRecord
|
|||
too_long = options[:too_long] % option_value.end
|
||||
|
||||
validates_each(attrs, options) do |record, attr, value|
|
||||
value = value.split(//) if value.kind_of?(String)
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
if value.nil? or value.size < option_value.begin
|
||||
record.errors.add(attr, too_short)
|
||||
elsif value.size > option_value.end
|
||||
|
@ -553,7 +557,7 @@ module ActiveRecord
|
|||
message = (options[:message] || options[message_options[option]]) % option_value
|
||||
|
||||
validates_each(attrs, options) do |record, attr, value|
|
||||
value = value.split(//) if value.kind_of?(String)
|
||||
value = options[:tokenizer].call(value) if value.kind_of?(String)
|
||||
record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value]
|
||||
end
|
||||
end
|
||||
|
@ -614,14 +618,20 @@ module ActiveRecord
|
|||
# class (which has a database table to query from).
|
||||
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
|
||||
|
||||
if value.nil? || (configuration[:case_sensitive] || !finder_class.columns_hash[attr_name.to_s].text?)
|
||||
is_text_column = finder_class.columns_hash[attr_name.to_s].text?
|
||||
|
||||
if !value.nil? && is_text_column
|
||||
value = value.to_s
|
||||
end
|
||||
|
||||
if value.nil? || (configuration[:case_sensitive] || !is_text_column)
|
||||
condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}"
|
||||
condition_params = [value]
|
||||
else
|
||||
# sqlite has case sensitive SELECT query, while MySQL/Postgresql don't.
|
||||
# Hence, this is needed only for sqlite.
|
||||
condition_sql = "LOWER(#{record.class.quoted_table_name}.#{attr_name}) #{attribute_condition(value)}"
|
||||
condition_params = [value.downcase]
|
||||
condition_params = [value.chars.downcase]
|
||||
end
|
||||
|
||||
if scope = configuration[:scope]
|
||||
|
@ -851,7 +861,7 @@ module ActiveRecord
|
|||
raw_value = raw_value.to_i
|
||||
else
|
||||
begin
|
||||
raw_value = Kernel.Float(raw_value.to_s)
|
||||
raw_value = Kernel.Float(raw_value)
|
||||
rescue ArgumentError, TypeError
|
||||
record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number])
|
||||
next
|
||||
|
|
|
@ -2,7 +2,7 @@ module ActiveRecord
|
|||
module VERSION #:nodoc:
|
||||
MAJOR = 2
|
||||
MINOR = 1
|
||||
TINY = 0
|
||||
TINY = 1
|
||||
|
||||
STRING = [MAJOR, MINOR, TINY].join('.')
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue