diff options
Diffstat (limited to 'activerecord/lib')
89 files changed, 1861 insertions, 1214 deletions
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 0d5313956b..45c275a017 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -230,8 +230,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? }) - attrs = mapping.collect {|pair| read_attribute(pair.first)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) @@ -249,10 +249,10 @@ module ActiveRecord end if part.nil? && allow_nil - mapping.each { |pair| self[pair.first] = nil } + mapping.each { |key, _| self[key] = nil } @aggregation_cache[name] = nil else - mapping.each { |pair| self[pair.first] = part.send(pair.last) } + mapping.each { |key, value| self[key] = part.send(value) } @aggregation_cache[name] = part.freeze end end diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 20516bba0c..5a84792f45 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -9,6 +9,10 @@ module ActiveRecord @association end + def ==(other) + other == to_a + end + private def exec_queries diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 4abe2ad0a0..727ee5f65f 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -419,6 +419,10 @@ module ActiveRecord # has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event' # end # + # Note: Joining, eager loading and preloading of these associations is not fully possible. + # These operations happen before instance creation and the scope will be called with a +nil+ argument. + # This can lead to unexpected behavior and is deprecated. + # # == Association callbacks # # Similar to the normal callbacks that hook into the life cycle of an Active Record object, @@ -773,11 +777,16 @@ module ActiveRecord # like this can have unintended consequences. # In the above example posts with no approved comments are not returned at all, because # the conditions apply to the SQL statement as a whole and not just to the association. + # # You must disambiguate column references for this fallback to happen, for example # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. # - # If you do want eager load only some members of an association it is usually more natural - # to include an association which has conditions defined on it: + # If you want to load all posts (including posts with no approved comments) then write + # your own LEFT OUTER JOIN query using ON + # + # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") + # + # In this case it is usually more natural to include an association which has conditions defined on it: # # class Post < ActiveRecord::Base # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment' @@ -1571,6 +1580,11 @@ module ActiveRecord join_model = builder.through_model + # FIXME: we should move this to the internal constants. Also people + # should never directly access this constant so I'm not happy about + # setting it. + const_set join_model.name, join_model + middle_reflection = builder.middle_reflection join_model Builder::HasMany.define_callbacks self, middle_reflection @@ -1590,7 +1604,7 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k| hm_options[k] = options[k] if options.key? k end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 27fd9e35db..572f556999 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,12 +1,34 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - INSTANCE = new - def self.scope(association, connection) INSTANCE.scope association, connection end + class BindSubstitution + def initialize(block) + @block = block + end + + def bind_value(scope, column, value, alias_tracker) + substitute = alias_tracker.connection.substitute_at( + column, scope.bind_values.length) + scope.bind_values += [[column, @block.call(value)]] + substitute + end + end + + def self.create(&block) + block = block ? block : lambda { |val| val } + new BindSubstitution.new(block) + end + + def initialize(bind_substitution) + @bind_substitution = bind_substitution + end + + INSTANCE = create + def scope(association, connection) klass = association.klass reflection = association.reflection @@ -22,6 +44,23 @@ module ActiveRecord Arel::Nodes::InnerJoin end + def self.get_bind_values(owner, chain) + bvs = [] + chain.each_with_index do |reflection, i| + if reflection == chain.last + bvs << reflection.join_id_for(owner) + if reflection.type + bvs << owner.class.base_class.name + end + else + if reflection.type + bvs << chain[i + 1].klass.base_class.name + end + end + end + bvs + end + private def construct_tables(chain, klass, refl, alias_tracker) @@ -49,10 +88,7 @@ module ActiveRecord end def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) - scope.bind_values += [[column, value]] - substitute + @bind_substitution.bind_value scope, column, value, alias_tracker end def bind(scope, table_name, column_name, value, tracker) @@ -120,6 +156,7 @@ module ActiveRecord end scope.where_values += item.where_values + scope.bind_values += item.bind_values scope.order_values |= item.order_values end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8272a5584c..1edd4fa3aa 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -31,6 +31,14 @@ module ActiveRecord @updated end + def decrement_counters # :nodoc: + with_cache_name { |name| decrement_counter name } + end + + def increment_counters # :nodoc: + with_cache_name { |name| increment_counter name } + end + private def find_target? @@ -51,13 +59,15 @@ module ActiveRecord end end - def decrement_counters - with_cache_name { |name| decrement_counter name } + def decrement_counter(counter_cache_name) + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) + end end - def decrement_counter counter_cache_name + def increment_counter(counter_cache_name) if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) + klass.increment_counter(counter_cache_name, target_id) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 5ccaa55a32..3998aca23e 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -26,28 +26,9 @@ module ActiveRecord::Associations::Builder private def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_create + return if mixin.method_defined? :belongs_to_counter_cache_after_update mixin.class_eval do - def belongs_to_counter_cache_after_create(reflection) - if record = send(reflection.name) - cache_column = reflection.counter_cache_column - record.class.increment_counter(cache_column, record.id) - @_after_create_counter_called = true - end - end - - def belongs_to_counter_cache_before_destroy(reflection) - foreign_key = reflection.foreign_key.to_sym - unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - record = send reflection.name - if record && !self.destroyed? - cache_column = reflection.counter_cache_column - record.class.decrement_counter(cache_column, record.id) - end - end - end - def belongs_to_counter_cache_after_update(reflection) foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column @@ -73,14 +54,6 @@ module ActiveRecord::Associations::Builder def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - model.after_create lambda { |record| - record.belongs_to_counter_cache_after_create(reflection) - } - - model.before_destroy lambda { |record| - record.belongs_to_counter_cache_before_destroy(reflection) - } - model.after_update lambda { |record| record.belongs_to_counter_cache_after_update(reflection) } @@ -130,7 +103,7 @@ module ActiveRecord::Associations::Builder BelongsTo.touch_record(record, foreign_key, n, touch) } - model.after_save callback + model.after_save callback, if: :changed? model.after_touch callback model.after_destroy callback end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index e472277374..30b11c01eb 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -11,7 +11,7 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") end private @@ -23,7 +23,13 @@ module ActiveRecord::Associations::Builder KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { - name.to_s.camelize.singularize + model_name = name.to_s.camelize.singularize + + if lhs_class.parent_name + model_name.prepend("#{lhs_class.parent_name}::") + end + + model_name } KnownClass.new lhs_class, class_name end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 7909b93622..4c8c826f76 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index dee7e972c1..c5f7bcae7d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -182,11 +182,11 @@ module ActiveRecord # # See delete for more info. def delete_all(dependent = nil) - if dependent.present? && ![:nullify, :delete_all].include?(dependent) + if dependent && ![:nullify, :delete_all].include?(dependent) raise ArgumentError, "Valid values are :nullify or :delete_all" end - dependent = if dependent.present? + dependent = if dependent dependent elsif options[:dependent] == :destroy :delete_all @@ -194,7 +194,7 @@ module ActiveRecord options[:dependent] end - delete(:all, dependent: dependent).tap do + delete_or_nullify_all_records(dependent).tap do reset loaded! end @@ -244,19 +244,12 @@ module ActiveRecord # are actually removed from the database, that depends precisely on # +delete_records+. They are in any case removed from the collection. def delete(*records) + return if records.empty? _options = records.extract_options! dependent = _options[:dependent] || options[:dependent] - if records.first == :all - if (loaded? || dependent == :destroy) && dependent != :delete_all - delete_or_destroy(load_target, dependent) - else - delete_records(:all, dependent) - end - else - records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } - delete_or_destroy(records, dependent) - end + records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } + delete_or_destroy(records, dependent) end # Deletes the +records+ and removes them from this association calling @@ -265,6 +258,7 @@ module ActiveRecord # Note that this method removes records from the database ignoring the # +:dependent+ option. def destroy(*records) + return if records.empty? records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) } delete_or_destroy(records, :destroy) end @@ -412,9 +406,23 @@ module ActiveRecord end private + def get_records + return scope.to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge as.scope(self, conn) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end def find_target - records = scope.to_a + records = get_records records.each { |record| set_inverse_instance(record) } records end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 5b71ed163e..84c8cfe72b 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -357,7 +357,7 @@ module ActiveRecord # Deletes all the records from the collection. For +has_many+ associations, # the deletion is done according to the strategy specified by the <tt>:dependent</tt> - # option. Returns an array with the deleted records. + # option. # # If no <tt>:dependent</tt> option is given, then it will follow the # default strategy. The default strategy is <tt>:nullify</tt>. This @@ -435,11 +435,6 @@ module ActiveRecord # # ] # # person.pets.delete_all - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 3e4b7902c0..f5e911c739 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -58,7 +58,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - owner.send(:read_attribute, cached_counter_attribute_name) + owner.read_attribute cached_counter_attribute_name else scope.count end @@ -105,23 +105,27 @@ module ActiveRecord } end + def delete_count(method, scope) + if method == :delete_all + scope.delete_all + else + scope.update_all(reflection.foreign_key => nil) + end + end + + def delete_or_nullify_all_records(method) + count = delete_count(method, self.scope) + update_counter(-count) + end + # Deletes the records according to the <tt>:dependent</tt> option. def delete_records(records, method) if method == :destroy records.each(&:destroy!) update_counter(-records.length) unless inverse_updates_counter_cache? else - if records == :all || !reflection.klass.primary_key - scope = self.scope - else - scope = self.scope.where(reflection.klass.primary_key => records) - end - - if method == :delete_all - update_counter(-scope.delete_all) - else - update_counter(-scope.update_all(reflection.foreign_key => nil)) - end + scope = self.scope.where(reflection.klass.primary_key => records) + update_counter(-delete_count(method, scope)) end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 64bc98c642..35ad512537 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -12,17 +12,18 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been - # loaded and calling collection.size if it has. If it's more likely than not that the collection does - # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer - # SELECT query if you use #length. + # Returns the size of the collection by executing a SELECT COUNT(*) query + # if the collection hasn't been loaded, and by calling collection.size if + # it has. If the collection will likely have a size greater than zero, + # and if fetching the collection will be needed afterwards, one less + # SELECT query will be generated by using #length instead. def size if has_cached_counter? - owner.send(:read_attribute, cached_counter_attribute_name) + owner.read_attribute cached_counter_attribute_name(reflection) elsif loaded? target.size else - count + super end end @@ -72,23 +73,25 @@ module ActiveRecord @through_association ||= owner.association(through_reflection.name) end - # We temporarily cache through record that has been build, because if we build a - # through record in build_record and then subsequently call insert_record, then we - # want to use the exact same object. + # The through record (built with build_record) is temporarily cached + # so that it may be reused if insert_record is subsequently called. # - # However, after insert_record has been called, we clear the cache entry because - # we want it to be possible to have multiple instances of the same record in an - # association + # However, after insert_record has been called, the cache is cleared in + # order to allow multiple instances of the same record in an association. def build_through_record(record) @through_records[record.object_id] ||= begin ensure_mutable - through_record = through_association.build + through_record = through_association.build through_scope_attributes through_record.send("#{source_reflection.name}=", record) through_record end end + def through_scope_attributes + scope.where_values_hash(through_association.reflection.name.to_s) + end + def save_through_record(record) build_through_record(record).save! ensure @@ -127,13 +130,13 @@ module ActiveRecord end end + def delete_or_nullify_all_records(method) + delete_records(load_target, method) + end + def delete_records(records, method) ensure_not_nested - # This is unoptimised; it will load all the target records - # even when we just want to delete everything. - records = load_target if records == :all - scope = through_association.scope scope.where! construct_join_attributes(*records) @@ -198,7 +201,7 @@ module ActiveRecord def find_target return [] unless target_reflection_has_associated_record? - scope.to_a + get_records end # NOTE - not sure that we can actually cope with inverses here diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 94f69d4c2d..5842be3a7b 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -164,17 +164,17 @@ module ActiveRecord def make_outer_joins(parent, child) tables = table_aliases_for(parent, child) join_type = Arel::Nodes::OuterJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_outer_joins(child, c) } + [info] + child.children.flat_map { |c| make_outer_joins(child, c) } end def make_inner_joins(parent, child) tables = child.tables join_type = Arel::Nodes::InnerJoin - joins = make_constraints parent, child, tables, join_type + info = make_constraints parent, child, tables, join_type - joins.concat child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_inner_joins(child, c) } end def table_aliases_for(parent, node) @@ -215,6 +215,7 @@ module ActiveRecord associations.map do |name, right| reflection = find_reflection base_klass, name reflection.check_validity! + reflection.check_eager_loadable! if reflection.options[:polymorphic] raise EagerLoadPolymorphicError.new(reflection) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index 1d923ecc09..a0e83c0a02 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -21,8 +21,11 @@ module ActiveRecord super && reflection == other.reflection end + JoinInformation = Struct.new :joins, :binds + def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) joins = [] + bind_values = [] tables = tables.reverse scope_chain_index = 0 @@ -60,21 +63,27 @@ module ActiveRecord left.merge right end - if reflection.type - constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name - end - if rel && !rel.arel.constraints.empty? + bind_values.concat rel.bind_values constraint = constraint.and rel.arel.constraints end + if reflection.type + value = foreign_klass.base_class.name + column = klass.columns_hash[column.to_s] + + substitute = klass.connection.substitute_at(column, bind_values.length) + bind_values.push [column, value] + constraint = constraint.and table[reflection.type].eq substitute + end + joins << table.create_join(table, table.create_on(constraint), join_type) # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end - joins + JoinInformation.new joins, bind_values end # Builds equality condition. diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 31ddf4e0fc..42571d6af0 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -80,7 +80,7 @@ module ActiveRecord # { author: :avatar } # [ :books, { author: :avatar } ] - NULL_RELATION = Struct.new(:values).new({}) + NULL_RELATION = Struct.new(:values, :bind_values).new({}, []) def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq @@ -112,13 +112,14 @@ module ActiveRecord end def preloaders_for_hash(association, records, scope) - parent, child = association.to_a.first # hash should only be of length 1 + association.flat_map { |parent, child| + loaders = preloaders_for_one parent, records, scope - loaders = preloaders_for_one parent, records, scope - - recs = loaders.flat_map(&:preloaded_records).uniq - loaders.concat Array.wrap(child).flat_map { |assoc| - preloaders_on assoc, recs, scope + recs = loaders.flat_map(&:preloaded_records).uniq + loaders.concat Array.wrap(child).flat_map { |assoc| + preloaders_on assoc, recs, scope + } + loaders } end @@ -175,6 +176,7 @@ module ActiveRecord if owners.first.association(reflection.name).loaded? return AlreadyLoaded end + reflection.check_preloadable! case reflection.macro when :has_many diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 69b65982b3..63773bd5e1 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -57,9 +57,15 @@ module ActiveRecord end def owners_by_key - @owners_by_key ||= owners.group_by do |owner| - owner[owner_key_name] - end + @owners_by_key ||= if key_conversion_required? + owners.group_by do |owner| + owner[owner_key_name].to_s + end + else + owners.group_by do |owner| + owner[owner_key_name] + end + end end def options @@ -93,13 +99,28 @@ module ActiveRecord records_by_owner end + def key_conversion_required? + association_key_type != owner_key_type + end + + def association_key_type + @klass.column_types[association_key_name.to_s].type + end + + def owner_key_type + @model.column_types[owner_key_name.to_s].type + end + def load_slices(slices) @preloaded_records = slices.flat_map { |slice| records_for(slice) } @preloaded_records.map { |record| - [record, record[association_key_name]] + key = record[association_key_name] + key = key.to_s if key_conversion_required? + + [record, key] } end @@ -111,12 +132,15 @@ module ActiveRecord scope = klass.unscoped values = reflection_scope.values + reflection_binds = reflection_scope.bind_values preload_values = preload_scope.values + preload_binds = preload_scope.bind_values scope.where_values = Array(values[:where]) + Array(preload_values[:where]) scope.references_values = Array(values[:references]) + Array(preload_values[:references]) + scope.bind_values = (reflection_binds + preload_binds) - scope.select! preload_values[:select] || values[:select] || table[Arel.star] + scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] if preload_values.key? :order diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 70e97432e4..1fed7f74e7 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -84,7 +84,7 @@ module ActiveRecord end scope.references! reflection_scope.values[:references] - scope.order! reflection_scope.values[:order] if scope.eager_loading? + scope = scope.order reflection_scope.values[:order] if scope.eager_loading? end scope diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 747bb5f1d6..f2e3a4e40f 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -38,8 +38,23 @@ module ActiveRecord scope.scope_for_create.stringify_keys.except(klass.primary_key) end + def get_records + return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) + + conn = klass.connection + sc = reflection.association_scope_cache(conn, owner) do + StatementCache.create(conn) { |params| + as = AssociationScope.create { params.bind } + target_scope.merge(as.scope(self, conn)).limit(1) + } + end + + binds = AssociationScope.get_bind_values(owner, reflection.chain) + sc.execute binds, klass, klass.connection + end + def find_target - if record = scope.take + if record = get_records.first set_inverse_instance record end end diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ba7d2a3782..f8a85b8a6f 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,9 +14,11 @@ module ActiveRecord def target_scope scope = super chain.drop(1).each do |reflection| + relation = reflection.klass.all + relation.merge!(reflection.scope) if reflection.scope + scope.merge!( - reflection.klass.all. - except(:select, :create_with, :includes, :preload, :joins, :eager_load) + relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end scope diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 30fa2c8ba5..816fb51942 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -11,6 +11,15 @@ module ActiveRecord # If the passed hash responds to <tt>permitted?</tt> method and the return value # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt> # exception is raised. + # + # cat = Cat.new(name: "Gorby", status: "yawning") + # cat.attributes # => { "name" => "Gorby", "status" => "yawning" } + # cat.assign_attributes(status: "sleeping") + # cat.attributes # => { "name" => "Gorby", "status" => "sleeping" } + # + # New attributes will be persisted in the database when the object is saved. + # + # Aliased to <tt>attributes=</tt>. def assign_attributes(new_attributes) if !new_attributes.respond_to?(:stringify_keys) raise ArgumentError, "When assigning attributes, you must pass a hash as an argument." @@ -140,7 +149,7 @@ module ActiveRecord end def read_time - # If column is a :time (and not :date or :timestamp) there is no need to validate if + # If column is a :time (and not :date or :datetime) there is no need to validate if # there are year/month/day fields if column.type == :time # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 4b1733619a..8bd51dc71f 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -326,13 +326,13 @@ module ActiveRecord # class Task < ActiveRecord::Base # end # - # person = Task.new(title: '', is_done: false) - # person.attribute_present?(:title) # => false - # person.attribute_present?(:is_done) # => true - # person.name = 'Francesco' - # person.is_done = true - # person.attribute_present?(:title) # => true - # person.attribute_present?(:is_done) # => true + # task = Task.new(title: '', is_done: false) + # task.attribute_present?(:title) # => false + # task.attribute_present?(:is_done) # => true + # task.title = 'Buy milk' + # task.is_done = true + # task.attribute_present?(:title) # => true + # task.attribute_present?(:is_done) # => true def attribute_present?(attribute) value = read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) @@ -359,6 +359,8 @@ module ActiveRecord # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing. # + # Note: +:id+ is always present. + # # Alias for the <tt>read_attribute</tt> method. # # class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index d01e9aea59..979dfb207e 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -35,7 +35,7 @@ module ActiveRecord extend ActiveSupport::Concern - ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] + ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :time, :date] included do class_attribute :attribute_types_cached_by_default, instance_writer: false @@ -52,7 +52,7 @@ module ActiveRecord end # Returns the attributes which are cached. By default time related columns - # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached. + # with datatype <tt>:datetime, :time, :date</tt> are cached. def cached_attributes @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index a1d2242f73..53a9c874bf 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -30,7 +30,8 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name+ - Optional, class name that the object type should be equal to. + # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # or a class name that the object type should be equal to. # # ==== Example # @@ -38,13 +39,23 @@ module ActiveRecord # class User < ActiveRecord::Base # serialize :preferences # end - def serialize(attr_name, class_name = Object) + # + # # Serialize preferences using JSON as coder. + # class User < ActiveRecord::Base + # serialize :preferences, JSON + # end + # + # # Serialize preferences as Hash using YAML coder. + # class User < ActiveRecord::Base + # serialize :preferences, Hash + # end + def serialize(attr_name, class_name_or_coder = Object) include Behavior - coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) } - class_name + coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + class_name_or_coder else - Coders::YAMLColumn.new(class_name) + Coders::YAMLColumn.new(class_name_or_coder) end # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index f168282ea3..dfebb2cf56 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -28,7 +28,7 @@ module ActiveRecord module ClassMethods protected - # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled. + # Defined for all +datetime+ attributes when +time_zone_aware_attributes+ are enabled. # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone. def define_method_attribute=(attr_name) if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name]) @@ -51,7 +51,7 @@ module ActiveRecord def create_time_zone_conversion_attribute?(name, column) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type || :timestamp == column.type) + (:datetime == column.type) end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index e9622ca0c1..1a4d2957ec 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -35,7 +35,7 @@ module ActiveRecord # # === One-to-one Example # - # class Post + # class Post < ActiveRecord::Base # has_one :author, autosave: true # end # @@ -76,7 +76,7 @@ module ActiveRecord # # When <tt>:autosave</tt> is not declared new children are saved when their parent is saved: # - # class Post + # class Post < ActiveRecord::Base # has_many :comments # :autosave option is not declared # end # @@ -95,20 +95,23 @@ module ActiveRecord # When <tt>:autosave</tt> is true all children are saved, no matter whether they # are new records or not: # - # class Post + # class Post < ActiveRecord::Base # has_many :comments, autosave: true # end # # post = Post.create(title: 'ruby rocks') # post.comments.create(body: 'hello world') # post.comments[0].body = 'hi everyone' - # post.save # => saves both post and comment, with 'hi everyone' as body + # post.comments.build(body: "good morning.") + # post.title += "!" + # post.save # => saves both post and comments. # # Destroying one of the associated models as part of the parent's save action # is as simple as marking it for destruction: # - # post.comments.last.mark_for_destruction - # post.comments.last.marked_for_destruction? # => true + # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]> + # post.comments[1].mark_for_destruction + # post.comments[1].marked_for_destruction? # => true # post.comments.length # => 2 # # Note that the model is _not_ yet removed from the database: @@ -301,7 +304,8 @@ module ActiveRecord def association_valid?(reflection, record) return true if record.destroyed? || record.marked_for_destruction? - unless valid = record.valid? + validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) + unless valid = record.valid?(validation_context) if reflection.options[:autosave] record.errors.each do |attribute, message| attribute = "#{reflection.name}.#{attribute}" @@ -377,15 +381,16 @@ module ActiveRecord def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy - else + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1d47cba234..db4d5f0129 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -296,7 +296,6 @@ module ActiveRecord #:nodoc: include Core include Persistence - include NoTouching include ReadonlyAttributes include ModelSchema include Inheritance @@ -318,6 +317,7 @@ module ActiveRecord #:nodoc: include NestedAttributes include Aggregations include Transactions + include NoTouching include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index da25e640c1..bc47412405 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -9,15 +9,23 @@ module ActiveRecord # Converts an arel AST to SQL def to_sql(arel, binds = []) if arel.respond_to?(:ast) - binds = binds.dup - visitor.accept(arel.ast) do - quote(*binds.shift.reverse) - end + collected = visitor.accept(arel.ast, collector) + collected.compile(binds.dup, self) else arel end end + # This is used in the StatementCache object. It returns an object that + # can be used to query the database repeatedly. + def cacheable_query(arel) # :nodoc: + if prepared_statements + ActiveRecord::StatementCache.query visitor, arel.ast + else + ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector + end + end + # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) arel, binds = binds_from_relation arel, binds @@ -381,7 +389,7 @@ module ActiveRecord end def binds_from_relation(relation, binds) - if relation.is_a?(Relation) && binds.blank? + if relation.is_a?(Relation) && binds.empty? relation, binds = relation.arel, relation.bind_values end [relation, binds] diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 71c3a4378b..117c0f0969 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -99,6 +99,8 @@ module ActiveRecord # Specifies the precision for a <tt>:decimal</tt> column. # * <tt>:scale</tt> - # Specifies the scale for a <tt>:decimal</tt> column. + # * <tt>:index</tt> - + # Create an index for the column. Can be either <tt>true</tt> or an options hash. # # For clarity's sake: the precision is the number of significant digits, # while the scale is the number of digits that can be stored following @@ -123,17 +125,8 @@ module ActiveRecord # Default is (38,0). # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. # Default unknown. - # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18]. - # Default (9,0). Internal types NUMERIC and DECIMAL have different - # storage rules, decimal being better. - # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for - # NUMERIC is 19, and DECIMAL is 38. # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. # Default (38,0). - # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. - # Default (38,0). - # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>. # # This method returns <tt>self</tt>. # @@ -172,18 +165,21 @@ module ActiveRecord # What can be written like this with the regular calls to column: # # create_table :products do |t| - # t.column :shop_id, :integer - # t.column :creator_id, :integer - # t.column :name, :string, default: "Untitled" - # t.column :value, :string, default: "Untitled" - # t.column :created_at, :datetime - # t.column :updated_at, :datetime + # t.column :shop_id, :integer + # t.column :creator_id, :integer + # t.column :item_number, :string + # t.column :name, :string, default: "Untitled" + # t.column :value, :string, default: "Untitled" + # t.column :created_at, :datetime + # t.column :updated_at, :datetime # end + # add_index :products, :item_number # # can also be written as follows using the short-hand: # # create_table :products do |t| # t.integer :shop_id, :creator_id + # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" # t.timestamps # end @@ -219,6 +215,8 @@ module ActiveRecord raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end + index_options = options.delete(:index) + index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options @columns_hash[name] = new_column_definition(name, type, options) self end @@ -264,6 +262,7 @@ module ActiveRecord alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: + type = aliased_types[type] || type column = create_column_definition name, type limit = options.fetch(:limit) do native[type][:limit] if native[type].is_a?(Hash) @@ -294,6 +293,12 @@ module ActiveRecord def native @native end + + def aliased_types + HashWithIndifferentAccess.new( + timestamp: :datetime, + ) + end end class AlterTable # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index aa99822389..ffa6af6d99 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -71,7 +71,8 @@ module ActiveRecord # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2) # def column_exists?(table_name, column_name, type = nil, options = {}) - columns(table_name).any?{ |c| c.name == column_name.to_s && + column_name = column_name.to_s + columns(table_name).any?{ |c| c.name == column_name && (!type || c.type == type) && (!options.key?(:limit) || c.limit == options[:limit]) && (!options.key?(:precision) || c.precision == options[:precision]) && diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ffd5055dec..0c55dbb037 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -3,9 +3,12 @@ require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' +require 'active_record/connection_adapters/type' require 'active_record/connection_adapters/abstract/schema_dumper' require 'active_record/connection_adapters/abstract/schema_creation' require 'monitor' +require 'arel/collectors/bind' +require 'arel/collectors/sql_string' module ActiveRecord module ConnectionAdapters # :nodoc: @@ -90,6 +93,8 @@ module ActiveRecord end end + attr_reader :prepared_statements + def initialize(connection, logger = nil, pool = nil) #:nodoc: super() @@ -103,6 +108,26 @@ module ActiveRecord @prepared_statements = false end + class BindCollector < Arel::Collectors::Bind + def compile(bvs, conn) + super(bvs.map { |bv| conn.quote(*bv.reverse) }) + end + end + + class SQLString < Arel::Collectors::SQLString + def compile(bvs, conn) + super(bvs) + end + end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + def valid_type?(type) true end @@ -128,16 +153,11 @@ module ActiveRecord @owner = nil end - def unprepared_visitor - self.class::BindSubstitution.new self - end - def unprepared_statement old_prepared_statements, @prepared_statements = @prepared_statements, false - old_visitor, @visitor = @visitor, unprepared_visitor yield ensure - @visitor, @prepared_statements = old_visitor, old_prepared_statements + @prepared_statements = old_prepared_statements end # Returns the human-readable name of the adapter. Use mixed case - one @@ -318,13 +338,14 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) node end def case_sensitive_comparison(table, attribute, column, value) - value = case_sensitive_modifier(value) unless value.nil? - table[attribute].eq(value) + table_attr = table[attribute] + value = case_sensitive_modifier(value, table_attr) unless value.nil? + table_attr.eq(value) end def case_insensitive_comparison(table, attribute, column, value) @@ -340,8 +361,48 @@ module ActiveRecord pool.checkin self end + def type_map # :nodoc: + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end + protected + def lookup_cast_type(sql_type) # :nodoc: + type_map.lookup(sql_type) + end + + def initialize_type_map(m) # :nodoc: + m.register_type %r(boolean)i, Type::Boolean.new + m.register_type %r(char)i, Type::String.new + m.register_type %r(binary)i, Type::Binary.new + m.alias_type %r(blob)i, 'binary' + m.register_type %r(text)i, Type::Text.new + m.alias_type %r(clob)i, 'text' + m.register_type %r(date)i, Type::Date.new + m.register_type %r(time)i, Type::Time.new + m.alias_type %r(timestamp)i, 'datetime' + m.register_type %r(datetime)i, Type::DateTime.new + m.alias_type %r(numeric)i, 'decimal' + m.alias_type %r(number)i, 'decimal' + m.register_type %r(float)i, Type::Float.new + m.alias_type %r(double)i, 'float' + m.register_type %r(int)i, Type::Integer.new + m.register_type(%r(decimal)i) do |sql_type| + if Type.extract_scale(sql_type) == 0 + Type::Integer.new + else + Type::Decimal.new + end + end + end + + def reload_type_map # :nodoc: + type_map.clear + initialize_type_map(type_map) + end + def translate_exception_class(e, sql) message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger @@ -368,7 +429,13 @@ module ActiveRecord end def without_prepared_statement?(binds) - !@prepared_statements || binds.empty? + !prepared_statements || binds.empty? + end + + def column_for(table_name, column_name) # :nodoc: + column_name = column_name.to_s + columns(table_name).detect { |c| c.name == column_name } || + raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 20eea208ec..86eb2a38d8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -56,11 +56,11 @@ module ActiveRecord class Column < ConnectionAdapters::Column # :nodoc: attr_reader :collation, :strict, :extra - def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "") + def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "") @strict = strict @collation = collation @extra = extra - super(name, default, sql_type, null) + super(name, default, cast_type, sql_type, null) end def extract_default(default) @@ -97,18 +97,6 @@ module ActiveRecord private - def simplified_type(field_type) - return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end - end - def extract_limit(sql_type) case sql_type when /^enum\((.+)\)/i @@ -129,6 +117,8 @@ module ActiveRecord when /^mediumint/i; 3 when /^smallint/i; 2 when /^tinyint/i; 1 + when /^float/i; 24 + when /^double/i; 53 else super end @@ -173,7 +163,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -183,21 +172,18 @@ module ActiveRecord INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - class BindSubstitution < Arel::Visitors::MySQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) super(connection, logger) @connection_options, @config = connection_options, config @quoted_column_names, @quoted_table_names = {}, {} + @visitor = Arel::Visitors::MySQL.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::MySQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -263,9 +249,9 @@ module ActiveRecord raise NotImplementedError end - # Overridden by the adapters to instantiate their specific Column type. - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, extra) + def new_column(field, default, sql_type, null, collation, extra = "") # :nodoc: + cast_type = lookup_cast_type(sql_type) + Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end # Must return the Mysql error number from the exception, if the exception has an @@ -318,6 +304,11 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== + def clear_cache! + super + reload_type_map + end + # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) log(sql, name) { @connection.query(sql) } @@ -610,7 +601,8 @@ module ActiveRecord pk_and_sequence && pk_and_sequence.first end - def case_sensitive_modifier(node) + def case_sensitive_modifier(node, table_attribute) + node = Arel::Nodes.build_quoted node, table_attribute Arel::Nodes::Bin.new(node) end @@ -644,6 +636,15 @@ module ActiveRecord protected + def initialize_type_map(m) + super + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans + m.alias_type %r(enum)i, 'varchar' + m.alias_type %r(set)i, 'varchar' + m.alias_type %r(year)i, 'integer' + m.alias_type %r(bit)i, 'binary' + end + # MySQL is too stupid to create a temporary table for use subquery, so we have # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) @@ -713,15 +714,13 @@ module ActiveRecord end def rename_column_sql(table_name, column_name, new_column_name) - options = { name: new_column_name } - - if column = columns(table_name).find { |c| c.name == column_name.to_s } - options[:default] = column.default - options[:null] = column.null - options[:auto_increment] = (column.extra == "auto_increment") - else - raise ActiveRecordError, "No such column: #{table_name}.#{column_name}" - end + column = column_for(table_name, column_name) + options = { + name: new_column_name, + default: column.default, + null: column.null, + auto_increment: column.extra == "auto_increment" + } current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"] schema_creation.accept ChangeColumnDefinition.new column, current_type, options @@ -759,30 +758,23 @@ module ActiveRecord version[0] >= 5 end - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column - end - def configure_connection - variables = @config[:variables] || {} + variables = @config.fetch(:variables, {}).stringify_keys # By default, MySQL 'where id is null' selects the last inserted id. # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variables[:sql_auto_is_null] = 0 + variables['sql_auto_is_null'] = 0 # Increase timeout so the server doesn't disconnect us. wait_timeout = @config[:wait_timeout] wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum) - variables[:wait_timeout] = self.class.type_cast_config_to_integer(wait_timeout) + variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout) # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?(:sql_mode) - variables[:sql_mode] = 'STRICT_ALL_TABLES' + if strict_mode? && !variables.has_key?('sql_mode') + variables['sql_mode'] = 'STRICT_ALL_TABLES' end # NAMES does not have an equals sign, see diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 187eefb9e4..a718756b93 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,104 +13,46 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function + attr_reader :name, :default, :cast_type, :limit, :null, :sql_type, :precision, :scale, :default_function attr_accessor :primary, :coder alias :encoded? :coder + delegate :type, :klass, :text?, :number?, :binary?, :type_cast_for_write, to: :cast_type + # Instantiates a new column in the table. # # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>. # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. + # +cast_type+ is the object used for type casting and type information. # +sql_type+ is used to extract the column's length, if necessary. For example +60+ in # <tt>company_name varchar(60)</tt>. # It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute. # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type = nil, null = true) + def initialize(name, default, cast_type, sql_type = nil, null = true) @name = name + @cast_type = cast_type @sql_type = sql_type @null = null @limit = extract_limit(sql_type) @precision = extract_precision(sql_type) @scale = extract_scale(sql_type) - @type = simplified_type(sql_type) @default = extract_default(default) @default_function = nil @primary = nil @coder = nil end - # Returns +true+ if the column is either of type string or text. - def text? - type == :string || type == :text - end - - # Returns +true+ if the column is either of type integer, float or decimal. - def number? - type == :integer || type == :float || type == :decimal - end - def has_default? !default.nil? end - # Returns the Ruby class that corresponds to the abstract data type. - def klass - case type - when :integer then Fixnum - when :float then Float - when :decimal then BigDecimal - when :datetime, :timestamp, :time then Time - when :date then Date - when :text, :string, :binary then String - when :boolean then Object - end - end - - def binary? - type == :binary - end - - # Casts a Ruby value to something appropriate for writing to the database. - def type_cast_for_write(value) - return value unless number? - - case value - when FalseClass - 0 - when TrueClass - 1 - when String - value.presence - else - value - end - end - # Casts value to an appropriate instance. def type_cast(value) - return nil if value.nil? - return coder.load(value) if encoded? - - klass = self.class - - case type - when :string, :text - case value - when TrueClass; "1" - when FalseClass; "0" - else - value.to_s - end - when :integer then klass.value_to_integer(value) - when :float then value.to_f - when :decimal then klass.value_to_decimal(value) - when :datetime, :timestamp then klass.string_to_time(value) - when :time then klass.string_to_dummy_time(value) - when :date then klass.value_to_date(value) - when :binary then klass.binary_to_string(value) - when :boolean then klass.value_to_boolean(value) - else value + if encoded? + coder.load(value) + else + cast_type.type_cast(value) end end @@ -126,132 +68,9 @@ module ActiveRecord type_cast(default) end - class << self - # Used to convert from BLOBs to Strings - def binary_to_string(value) - value - end - - def value_to_date(value) - if value.is_a?(String) - return nil if value.empty? - fast_string_to_date(value) || fallback_string_to_date(value) - elsif value.respond_to?(:to_date) - value.to_date - else - value - end - end - - def string_to_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - fast_string_to_time(string) || fallback_string_to_time(string) - end - - def string_to_dummy_time(string) - return string unless string.is_a?(String) - return nil if string.empty? - - dummy_time_string = "2000-01-01 #{string}" - - fast_string_to_time(dummy_time_string) || begin - time_hash = Date._parse(dummy_time_string) - return nil if time_hash[:hour].nil? - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) - end - end - - # convert something to a boolean - def value_to_boolean(value) - if value.is_a?(String) && value.empty? - nil - else - TRUE_VALUES.include?(value) - end - end - - # Used to convert values to integer. - # handle the case when an integer column is used to store boolean values - def value_to_integer(value) - case value - when TrueClass, FalseClass - value ? 1 : 0 - else - value.to_i rescue nil - end - end - - # convert something to a BigDecimal - def value_to_decimal(value) - # Using .class is faster than .is_a? and - # subclasses of BigDecimal will be handled - # in the else clause - if value.class == BigDecimal - value - elsif value.respond_to?(:to_d) - value.to_d - else - value.to_s.to_d - end - end - - protected - # '0.123456' -> 123456 - # '1.123456' -> 123456 - def microseconds(time) - time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 - end - - def new_date(year, mon, mday) - if year && year != 0 - Date.new(year, mon, mday) rescue nil - end - end - - def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) - # Treat 0000-00-00 00:00:00 as nil. - return nil if year.nil? || (year == 0 && mon == 0 && mday == 0) - - if offset - time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil - return nil unless time - - time -= offset - Base.default_timezone == :utc ? time : time.getlocal - else - Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil - end - end - - def fast_string_to_date(string) - if string =~ Format::ISO_DATE - new_date $1.to_i, $2.to_i, $3.to_i - end - end - - # Doesn't handle time zones. - def fast_string_to_time(string) - if string =~ Format::ISO_DATETIME - microsec = ($7.to_r * 1_000_000).to_i - new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec - end - end - - def fallback_string_to_date(string) - new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) - end - - def fallback_string_to_time(string) - time_hash = Date._parse(string) - time_hash[:sec_fraction] = microseconds(time_hash) - - new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) - end - end - private + delegate :extract_scale, to: :cast_type + def extract_limit(sql_type) $1.to_i if sql_type =~ /\((.*)\)/ end @@ -259,40 +78,6 @@ module ActiveRecord def extract_precision(sql_type) $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i end - - def extract_scale(sql_type) - case sql_type - when /^(numeric|decimal|number)\((\d+)\)/i then 0 - when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i - end - end - - def simplified_type(field_type) - case field_type - when /int/i - :integer - when /float|double/i - :float - when /decimal|numeric|number/i - extract_scale(field_type) == 0 ? :integer : :decimal - when /datetime/i - :datetime - when /timestamp/i - :timestamp - when /time/i - :time - when /date/i - :date - when /clob/i, /text/i - :text - when /blob/i, /binary/i - :binary - when /char/i - :string - when /boolean/i - :boolean - end - end end end # :startdoc: diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index e0715f7ce9..b79d1a4458 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -33,7 +33,7 @@ module ActiveRecord def initialize(url) raise "Database URL cannot be empty" if url.blank? @uri = URI.parse(url) - @adapter = @uri.scheme + @adapter = @uri.scheme.gsub('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -62,8 +62,8 @@ module ActiveRecord # Converts the query parameters of the URI into a hash. # - # "localhost?pool=5&reap_frequency=2" - # # => { "pool" => "5", "reap_frequency" => "2" } + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } # # returns empty hash if no query present. # @@ -220,10 +220,10 @@ module ActiveRecord # an environment key or a URL spec, so we have deprecated # this ambiguous behaviour and in the future this function # can be removed in favor of resolve_url_connection. - if configurations.key?(spec) + if configurations.key?(spec) || spec !~ /:/ ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" - resolve_connection(configurations[spec]) + resolve_symbol_connection(spec) else resolve_url_connection(spec) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 5e82fdcbe0..0a14cdfe89 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,18 +29,11 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column # :nodoc: - def adapter - Mysql2Adapter - end - end - ADAPTER_NAME = 'Mysql2' def initialize(connection, logger, connection_options, config) super - @visitor = BindSubstitution.new self + @prepared_statements = false configure_connection end @@ -69,10 +62,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) exception.error_number if exception.respond_to?(:error_number) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index e6aa2ba921..8beb869ce2 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -66,35 +66,6 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - - class Column < AbstractMysqlAdapter::Column #:nodoc: - def self.string_to_time(value) - return super unless Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - end - - def self.string_to_dummy_time(v) - return super unless Mysql::Time === v - new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part) - end - - def self.string_to_date(v) - return super unless Mysql::Time === v - new_date(v.year, v.month, v.day) - end - - def adapter - MysqlAdapter - end - end - ADAPTER_NAME = 'MySQL' class StatementPool < ConnectionAdapters::StatementPool @@ -156,10 +127,6 @@ module ActiveRecord end end - def new_column(field, default, type, null, collation, extra = "") # :nodoc: - Column.new(field, default, type, null, collation, strict_mode?, extra) - end - def error_number(exception) # :nodoc: exception.errno if exception.respond_to?(:errno) end @@ -222,6 +189,7 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! + super @statements.clear end @@ -295,125 +263,69 @@ module ActiveRecord end module Fields - class Type - def type; end - - def type_cast_for_write(value) - value - end - end - - class Identity < Type - def type_cast(value); value; end - end - - class Integer < Type - def type_cast(value) - return if value.nil? - - value.to_i rescue value ? 1 : 0 - end - end - - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.value_to_date value - end - end - - class DateTime < Type - def type; :datetime; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_time value - end - end - - class Time < Type - def type; :time; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is mysql - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end - end - - class Float < Type - def type; :float; end - - def type_cast(value) - return if value.nil? - - value.to_f + class DateTime < Type::DateTime + def cast_value(value) + if Mysql::Time === value + new_time( + value.year, + value.month, + value.day, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Decimal < Type - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value + class Time < Type::Time + def cast_value(value) + if Mysql::Time === value + new_time( + 2000, + 01, + 01, + value.hour, + value.minute, + value.second, + value.second_part) + else + super + end end end - class Boolean < Type - def type_cast(value) - return if value.nil? + class << self + TYPES = ConnectionAdapters::Type::HashLookupTypeMap.new # :nodoc: - ConnectionAdapters::Column.value_to_boolean value - end - end - - TYPES = {} - - # Register an MySQL +type_id+ with a typecasting object in - # +type+. - def self.register_type(type_id, type) - TYPES[type_id] = type - end + delegate :register_type, :alias_type, to: :TYPES - def self.alias_type(new, old) - TYPES[new] = TYPES[old] - end - - def self.find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES[Mysql::Field::TYPE_LONG] - else - TYPES.fetch(field.type) { Fields::Identity.new } + def find_type(field) + if field.type == Mysql::Field::TYPE_TINY && field.length > 1 + TYPES.lookup(Mysql::Field::TYPE_LONG) + else + TYPES.lookup(field.type) + end end end - register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new - register_type Mysql::Field::TYPE_LONG, Fields::Integer.new + register_type Mysql::Field::TYPE_TINY, Type::Boolean.new + register_type Mysql::Field::TYPE_LONG, Type::Integer.new alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new - register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new - register_type Mysql::Field::TYPE_DATE, Fields::Date.new + register_type Mysql::Field::TYPE_DATE, Type::Date.new register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new + register_type Mysql::Field::TYPE_FLOAT, Type::Float.new + end - Mysql::Field.constants.grep(/TYPE/).map { |class_name| - Mysql::Field.const_get class_name - }.reject { |const| TYPES.key? const }.each do |const| - register_type const, Fields::Identity.new - end + def initialize_type_map(m) # :nodoc: + super + m.register_type %r(datetime)i, Fields::DateTime.new + m.register_type %r(time)i, Fields::Time.new end def exec_without_stmt(sql, name = 'SQL') # :nodoc: @@ -431,7 +343,7 @@ module ActiveRecord fields << field_name if field.decimals > 0 - types[field_name] = Fields::Decimal.new + types[field_name] = Type::Decimal.new else types[field_name] = Fields.find_type field end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index 0b218f2bfd..1b74c039ce 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLColumn < Column - module ArrayParser + module PostgreSQL + module ArrayParser # :nodoc: DOUBLE_QUOTE = '"' BACKSLASH = "\\" @@ -9,35 +9,23 @@ module ActiveRecord BRACKET_OPEN = '{' BRACKET_CLOSE = '}' - private - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - def parse_pg_array(string) - parse_data(string) + def parse_pg_array(string) # :nodoc: + local_index = 0 + array = [] + while(local_index < string.length) + case string[local_index] + when BRACKET_OPEN + local_index,array = parse_array_contents(array, string, local_index + 1) + when BRACKET_CLOSE + return array end + local_index += 1 end - def parse_data(string) - local_index = 0 - array = [] - while(local_index < string.length) - case string[local_index] - when BRACKET_OPEN - local_index,array = parse_array_contents(array, string, local_index + 1) - when BRACKET_CLOSE - return array - end - local_index += 1 - end + array + end - array - end + private def parse_array_contents(array, string, index) is_escaping = false diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 551a9289c3..0cbedb0987 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -1,32 +1,19 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLColumn < Column - module Cast - def point_to_string(point) + module PostgreSQL + module Cast # :nodoc: + def point_to_string(point) # :nodoc: "(#{point[0]},#{point[1]})" end - def string_to_point(string) + def string_to_point(string) # :nodoc: if string[0] == '(' && string[-1] == ')' string = string[1...-1] end string.split(',').map{ |v| Float(v) } end - def string_to_time(string) - return string unless String === string - - case string - when 'infinity'; Float::INFINITY - when '-infinity'; -Float::INFINITY - when / BC$/ - super("-" + string.sub(/ BC$/, "")) - else - super - end - end - - def string_to_bit(value) + def string_to_bit(value) # :nodoc: case value when /^0x/i value[2..-1].hex.to_s(2) # Hexadecimal notation @@ -35,7 +22,7 @@ module ActiveRecord end end - def hstore_to_string(object, array_member = false) + def hstore_to_string(object, array_member = false) # :nodoc: if Hash === object string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') string = escape_hstore(string) if array_member @@ -45,7 +32,7 @@ module ActiveRecord end end - def string_to_hstore(string) + def string_to_hstore(string) # :nodoc: if string.nil? nil elsif String === string @@ -59,7 +46,7 @@ module ActiveRecord end end - def json_to_string(object) + def json_to_string(object) # :nodoc: if Hash === object || Array === object ActiveSupport::JSON.encode(object) else @@ -67,7 +54,7 @@ module ActiveRecord end end - def array_to_string(value, column, adapter) + def array_to_string(value, column, adapter) # :nodoc: casted_values = value.map do |val| if String === val if val == "NULL" @@ -82,13 +69,13 @@ module ActiveRecord "{#{casted_values.join(',')}}" end - def range_to_string(object) + def range_to_string(object) # :nodoc: from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" end - def string_to_json(string) + def string_to_json(string) # :nodoc: if String === string ActiveSupport::JSON.decode(string) else @@ -96,7 +83,7 @@ module ActiveRecord end end - def string_to_cidr(string) + def string_to_cidr(string) # :nodoc: if string.nil? nil elsif String === string @@ -110,7 +97,7 @@ module ActiveRecord end end - def cidr_to_string(object) + def cidr_to_string(object) # :nodoc: if IPAddr === object "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else @@ -118,7 +105,7 @@ module ActiveRecord end end - def string_to_array(string, oid) + def string_to_array(string, oid) # :nodoc: parse_pg_array(string).map {|val| type_cast_array(oid, val)} end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 2cbcd5fd50..95f52312a5 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,3 +1,5 @@ +require 'active_record/connection_adapters/postgresql/cast' + module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. @@ -10,27 +12,31 @@ module ActiveRecord if sql_type =~ /\[\]$/ @array = true - super(name, default_value, sql_type[0..sql_type.length - 3], null) + super(name, default_value, oid_type, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, default_value, sql_type, null) + super(name, default_value, oid_type, sql_type, null) end @default_function = default if has_default_function?(default_value, default) end - def number? - !array && super - end - - def text? - !array && super - end - # :stopdoc: class << self - include ConnectionAdapters::PostgreSQLColumn::Cast - include ConnectionAdapters::PostgreSQLColumn::ArrayParser + include PostgreSQL::Cast + + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + attr_accessor :money_precision end # :startdoc: @@ -98,21 +104,6 @@ module ActiveRecord end end - def type_cast_for_write(value) - if @oid_type.respond_to?(:type_cast_for_write) - @oid_type.type_cast_for_write(value) - else - super - end - end - - def type_cast(value) - return if value.nil? - return super if encoded? - - @oid_type.type_cast value - end - def accessor @oid_type.accessor end @@ -132,12 +123,6 @@ module ActiveRecord end end - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - # Extracts the precision from PostgreSQL-specific data types. def extract_precision(sql_type) if sql_type == 'money' @@ -148,11 +133,6 @@ module ActiveRecord super end end - - # Maps PostgreSQL-specific data types to logical Rails types. - def simplified_type(field_type) - @oid_type.simplified_type(field_type) || super - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 51ee2829b2..89a7257d77 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" @@ -44,10 +44,32 @@ module ActiveRecord end end + def select_value(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 + end + end + + def select_values(arel, name = nil) + arel, binds = binds_from_relation arel, [] + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + if result.nfields > 0 + result.column_values(0) + else + [] + end + end + end + # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + execute_and_clear(sql, name, binds) do |result| + result.values + end end # Executes an INSERT query and returns the new record's ID @@ -72,6 +94,11 @@ module ActiveRecord super.insert end + # The internal PostgreSQL identifier of the money data type. + MONEY_COLUMN_TYPE_OID = 790 #:nodoc: + # The internal PostgreSQL identifier of the BYTEA data type. + BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: + # create a 2D array representing the result set def result_as_array(res) #:nodoc: # check if we have any binary column and if they need escaping @@ -134,31 +161,20 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - - types = {} - fields = result.fields - fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = type_map.fetch(ftype, fmod) { |oid, mod| - warn "unknown OID: #{fname}(#{oid}) (#{sql})" - OID::Identity.new - } + execute_and_clear(sql, name, binds) do |result| + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = get_oid_type(ftype, fmod, fname) + end + ActiveRecord::Result.new(fields, result.values, types) end - - ret = ActiveRecord::Result.new(fields, result.values, types) - result.clear - return ret end def exec_delete(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - affected = result.cmd_tuples - result.clear - affected + execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } end alias :exec_update :exec_delete diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 9e898015a6..1d0384c3df 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,49 +1,26 @@ -require 'active_record/connection_adapters/abstract_adapter' - module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module OID - class Type - def type; end - def simplified_type(sql_type); type end - + module PostgreSQL + module OID # :nodoc: + module Infinity def infinity(options = {}) - ::Float::INFINITY * (options[:negative] ? -1 : 1) - end - end - - class Identity < Type - def type_cast(value) - value - end - end - - class String < Type - def type; :string end - - def type_cast(value) - return if value.nil? - - value.to_s + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY end end - class SpecializedString < OID::String - def type; @type end + class SpecializedString < Type::String + attr_reader :type def initialize(type) @type = type end - end - class Text < OID::String - def type; :text end + def text? + false + end end - class Bit < Type - def type; :string end - + class Bit < Type::String def type_cast(value) if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_bit value @@ -53,20 +30,20 @@ module ActiveRecord end end - class Bytea < Type - def type; :binary end - - def type_cast(value) - return if value.nil? + class Bytea < Type::Binary + def cast_value(value) PGconn.unescape_bytea value end end - class Money < Type - def type; :decimal end + class Money < Type::Decimal + include Infinity - def type_cast(value) - return if value.nil? + def extract_scale(sql_type) + 2 + end + + def cast_value(value) return value unless ::String === value # Because money output is formatted according to the locale, there are two @@ -85,11 +62,11 @@ module ActiveRecord value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') end - ConnectionAdapters::Column.value_to_decimal value + super(value) end end - class Vector < Type + class Vector < Type::Value attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types @@ -108,9 +85,7 @@ module ActiveRecord end end - class Point < Type - def type; :string end - + class Point < Type::String def type_cast(value) if ::String === value ConnectionAdapters::PostgreSQLColumn.string_to_point value @@ -120,10 +95,10 @@ module ActiveRecord end end - class Array < Type - def type; @subtype.type end - + class Array < Type::Value attr_reader :subtype + delegate :type, to: :subtype + def initialize(subtype) @subtype = subtype end @@ -137,12 +112,12 @@ module ActiveRecord end end - class Range < Type - attr_reader :subtype - def simplified_type(sql_type); sql_type.to_sym end + class Range < Type::Value + attr_reader :subtype, :type - def initialize(subtype) + def initialize(subtype, type) @subtype = subtype + @type = type end def extract_bounds(value) @@ -163,8 +138,8 @@ module ActiveRecord infinity?(value) ? value : @subtype.type_cast(value) end - def type_cast(value) - return if value.nil? || value == 'empty' + def cast_value(value) + return if value == 'empty' return value if value.is_a?(::Range) extracted = extract_bounds(value) @@ -186,112 +161,77 @@ This is not reliable and will be removed in the future. end end - class Integer < Type - def type; :integer end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end + class Integer < Type::Integer + include Infinity end - class Boolean < Type - def type; :boolean end + class DateTime < Type::DateTime + include Infinity - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_boolean value - end - end - - class Timestamp < Type - def type; :timestamp; end - def simplified_type(sql_type) - case sql_type - when /^timestamp with(?:out)? time zone$/ - :datetime + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + super("-" + value.sub(/ BC$/, "")) + else + super + end else - :timestamp + value end end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::PostgreSQLColumn.string_to_time value - end end - class Date < Type - def type; :date; end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.value_to_date value - end + class Date < Type::Date + include Infinity end - class Time < Type - def type; :time end - - def type_cast(value) - return if value.nil? - - # FIXME: probably we can improve this since we know it is PG - # specific - ConnectionAdapters::Column.string_to_dummy_time value - end + class Time < Type::Time + include Infinity end - class Float < Type - def type; :float end + class Float < Type::Float + include Infinity def type_cast(value) - return if value.nil? - - value.to_f + case value + when nil then nil + when 'Infinity' then ::Float::INFINITY + when '-Infinity' then -::Float::INFINITY + when 'NaN' then ::Float::NAN + else value.to_f + end end end - class Decimal < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_decimal value - end - + class Decimal < Type::Decimal def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end end - class Enum < Type - def type; :enum end + class Enum < Type::Value + def type + :enum + end def type_cast(value) value.to_s end end - class Hstore < Type - def type; :hstore end + class Hstore < Type::Value + def type + :hstore + end def type_cast_for_write(value) ConnectionAdapters::PostgreSQLColumn.hstore_to_string value end - def type_cast(value) - return if value.nil? - + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_hstore value end @@ -300,28 +240,32 @@ This is not reliable and will be removed in the future. end end - class Cidr < Type - def type; :cidr end - def type_cast(value) - return if value.nil? + class Cidr < Type::Value + def type + :cidr + end + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_cidr value end end + class Inet < Cidr - def type; :inet end + def type + :inet + end end - class Json < Type - def type; :json end + class Json < Type::Value + def type + :json + end def type_cast_for_write(value) ConnectionAdapters::PostgreSQLColumn.json_to_string value end - def type_cast(value) - return if value.nil? - + def cast_value(value) ConnectionAdapters::PostgreSQLColumn.string_to_json value end @@ -330,47 +274,84 @@ This is not reliable and will be removed in the future. end end - class Uuid < Type - def type; :uuid end + class Uuid < Type::Value + def type + :uuid + end + def type_cast(value) value.presence end end - class TypeMap - def initialize - @mapping = {} + # This class uses the data from PostgreSQL pg_type table to build + # the OID -> Type mapping. + # - OID is and integer representing the type. + # - Type is an OID::Type object. + # This class has side effects on the +store+ passed during initialization. + class TypeMapInitializer # :nodoc: + def initialize(store) + @store = store end - def []=(oid, type) - @mapping[oid] = type + def run(records) + mapped, nodes = records.partition { |row| OID.registered_type? row['typname'] } + ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' } + enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } + domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } + arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } + composites, nodes = nodes.partition { |row| row['typelem'] != '0' } + + mapped.each { |row| register_mapped_type(row) } + enums.each { |row| register_enum_type(row) } + domains.each { |row| register_domain_type(row) } + arrays.each { |row| register_array_type(row) } + ranges.each { |row| register_range_type(row) } + composites.each { |row| register_composite_type(row) } end - def [](oid) - @mapping[oid] + private + def register_mapped_type(row) + register row['oid'], OID::NAMES[row['typname']] end - def clear - @mapping.clear + def register_enum_type(row) + register row['oid'], OID::Enum.new end - def key?(oid) - @mapping.key? oid + def register_array_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Array.new(subtype) + end end - def fetch(ftype, fmod) - # The type for the numeric depends on the width of the field, - # so we'll do something special here. - # - # When dealing with decimal columns: - # - # places after decimal = fmod - 4 & 0xffff - # places before decimal = (fmod - 4) >> 16 & 0xffff - if ftype == 1700 && (fmod - 4 & 0xffff).zero? - ftype = 23 + def register_range_type(row) + if subtype = @store.lookup(row['rngsubtype'].to_i) + register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) end + end + + def register_domain_type(row) + if base_type = @store.lookup(row["typbasetype"].to_i) + register row['oid'], base_type + else + warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}." + end + end + + def register_composite_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Vector.new(row['typdelim'], subtype) + end + end + + def register(oid, oid_type) + oid = oid.to_i + + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + return if @store.key?(oid) - @mapping.fetch(ftype) { |oid| yield oid, fmod } + @store.register_type(oid, oid_type) end end @@ -379,7 +360,7 @@ This is not reliable and will be removed in the future. # type_map is then dynamically built with oids as the key and type # objects as values. NAMES = Hash.new { |h,k| # :nodoc: - h[k] = OID::Identity.new + h[k] = Type::Value.new } # Register an OID type named +name+ with a typecasting object in @@ -406,14 +387,15 @@ This is not reliable and will be removed in the future. register_type 'numeric', OID::Decimal.new register_type 'float4', OID::Float.new alias_type 'float8', 'float4' - register_type 'text', OID::Text.new - register_type 'varchar', OID::String.new + register_type 'text', Type::Text.new + register_type 'varchar', Type::String.new alias_type 'char', 'varchar' + alias_type 'name', 'varchar' alias_type 'bpchar', 'varchar' - register_type 'bool', OID::Boolean.new + register_type 'bool', Type::Boolean.new register_type 'bit', OID::Bit.new alias_type 'varbit', 'bit' - register_type 'timestamp', OID::Timestamp.new + register_type 'timestamp', OID::DateTime.new alias_type 'timestamptz', 'timestamp' register_type 'date', OID::Date.new register_type 'time', OID::Time.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 210172cf32..ad12298013 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -1,17 +1,17 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module Quoting # Escapes binary strings for bytea input to the database. def escape_bytea(value) - PGconn.escape_bytea(value) if value + @connection.escape_bytea(value) if value end # Unescapes bytea output from a database to the binary string it represents. # NOTE: This is NOT an inverse of escape_bytea! This is only to be used # on escaped binary output from database drive. def unescape_bytea(value) - PGconn.unescape_bytea(value) if value + @connection.unescape_bytea(value) if value end # Quotes PostgreSQL-specific data types for SQL input. @@ -150,13 +150,11 @@ module ActiveRecord # - "schema.name".table_name # - "schema.name"."table.name" def quote_table_name(name) - schema, name_part = extract_pg_identifier_from_name(name.to_s) - - unless name_part - quote_column_name(schema) + schema, table = Utils.extract_schema_and_table(name.to_s) + if schema + "#{quote_column_name(schema)}.#{quote_column_name(table)}" else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" + quote_column_name(table) end end @@ -182,6 +180,15 @@ module ActiveRecord end result end + + # Does not quote function default values for UUID columns + def quote_default_value(value, column) #:nodoc: + if column.type == :uuid && value =~ /\(\)/ + value + else + quote(value, column) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb index bc775394a6..52b307c432 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb @@ -1,12 +1,12 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module ReferentialIntegrity - def supports_disable_referential_integrity? #:nodoc: + module PostgreSQL + module ReferentialIntegrity # :nodoc: + def supports_disable_referential_integrity? # :nodoc: true end - def disable_referential_integrity #:nodoc: + def disable_referential_integrity # :nodoc: if supports_disable_referential_integrity? begin execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 50a73aa666..539ba38c4a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation private @@ -12,7 +12,7 @@ module ActiveRecord def visit_ColumnDefinition(o) sql = super - if o.primary_key? && o.type == :uuid + if o.primary_key? && o.type != :primary_key sql << " PRIMARY KEY " add_column_options!(sql, column_options(o)) end @@ -33,10 +33,6 @@ module ActiveRecord end end - def schema_creation - SchemaCreation.new self - end - module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -182,9 +178,7 @@ module ActiveRecord def columns(table_name) # Limit, precision, and scale are all handled by the superclass. column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| - oid = type_map.fetch(oid.to_i, fmod.to_i) { - OID::Identity.new - } + oid = get_oid_type(oid.to_i, fmod.to_i, column_name) PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') end end @@ -406,13 +400,16 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default(table_name, column_name, default) clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + column = column_for(table_name, column_name) + + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column end def change_column_null(table_name, column_name, null, default = nil) clear_cache! 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") + column = column_for(table_name, column_name) + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb new file mode 100644 index 0000000000..60ffd3a114 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -0,0 +1,25 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module Utils # :nodoc: + extend self + + # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. + # +schema_name+ is nil if not specified in +name+. + # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) + # +name+ supports the range of schema/table references understood by PostgreSQL, for example: + # + # * <tt>table_name</tt> + # * <tt>"table.name"</tt> + # * <tt>schema_name.table_name</tt> + # * <tt>schema_name."table.name"</tt> + # * <tt>"schema_name".table_name</tt> + # * <tt>"schema.name"."table name"</tt> + def extract_schema_and_table(name) + table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse + [schema, table] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 3510e4f3b0..ed3e884455 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,13 +1,14 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' + +require 'active_record/connection_adapters/postgresql/utils' +require 'active_record/connection_adapters/postgresql/column' require 'active_record/connection_adapters/postgresql/oid' -require 'active_record/connection_adapters/postgresql/cast' -require 'active_record/connection_adapters/postgresql/array_parser' require 'active_record/connection_adapters/postgresql/quoting' +require 'active_record/connection_adapters/postgresql/referential_integrity' require 'active_record/connection_adapters/postgresql/schema_statements' require 'active_record/connection_adapters/postgresql/database_statements' -require 'active_record/connection_adapters/postgresql/referential_integrity' -require 'active_record/connection_adapters/postgresql/column' + require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values @@ -215,7 +216,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, - timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, daterange: { name: "daterange" }, @@ -238,10 +238,12 @@ module ActiveRecord citext: { name: "citext" } } - include Quoting - include ReferentialIntegrity - include SchemaStatements - include DatabaseStatements + OID = PostgreSQL::OID #:nodoc: + + include PostgreSQL::Quoting + include PostgreSQL::ReferentialIntegrity + include PostgreSQL::SchemaStatements + include PostgreSQL::DatabaseStatements include Savepoints # Returns 'PostgreSQL' as adapter name for identification purposes. @@ -249,6 +251,10 @@ module ActiveRecord ADAPTER_NAME end + def schema_creation + PostgreSQL::SchemaCreation.new self + end + # Adds `:array` option to the default set provided by the # AbstractAdapter def prepare_column_options(column, types) @@ -338,19 +344,15 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc: - include Arel::Visitors::BindVisitor - end - # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) + @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::PostgreSQL.new self else - @visitor = unprepared_visitor + @prepared_statements = false end @connection_parameters, @config = connection_parameters, config @@ -367,7 +369,7 @@ module ActiveRecord raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" end - @type_map = OID::TypeMap.new + @type_map = Type::HashLookupTypeMap.new initialize_type_map(type_map) @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true @@ -498,25 +500,6 @@ module ActiveRecord exec_query "SET SESSION AUTHORIZATION #{user}" end - module Utils - extend self - - # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. - # +schema_name+ is nil if not specified in +name+. - # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) - # +name+ supports the range of schema/table references understood by PostgreSQL, for example: - # - # * <tt>table_name</tt> - # * <tt>"table.name"</tt> - # * <tt>schema_name.table_name</tt> - # * <tt>schema_name."table.name"</tt> - # * <tt>"schema.name"."table name"</tt> - def extract_schema_and_table(name) - table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse - [schema, table] - end - end - def use_insert_returning? @use_insert_returning end @@ -555,96 +538,67 @@ module ActiveRecord private - def type_map - @type_map - end + def get_oid_type(oid, fmod, column_name) + if !type_map.key?(oid) + initialize_type_map(type_map, [oid]) + end - def reload_type_map - type_map.clear - initialize_type_map(type_map) + type_map.fetch(normalize_oid_type(oid, fmod)) { + warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end + } end - def add_oid(row, records_by_oid, type_map) - return type_map if type_map.key? row['type_elem'].to_i - - if OID.registered_type? row['typname'] - # this composite type is explicitly registered - vector = OID::NAMES[row['typname']] + def normalize_oid_type(ftype, fmod) + # The type for the numeric depends on the width of the field, + # so we'll do something special here. + # + # When dealing with decimal columns: + # + # places after decimal = fmod - 4 & 0xffff + # places before decimal = (fmod - 4) >> 16 & 0xffff + if ftype == 1700 && (fmod - 4 & 0xffff).zero? + 23 else - # use the default for composite types - unless type_map.key? row['typelem'].to_i - add_oid records_by_oid[row['typelem']], records_by_oid, type_map - end - - vector = OID::Vector.new row['typdelim'], type_map[row['typelem'].to_i] + ftype end - - type_map[row['oid'].to_i] = vector - type_map end - def initialize_type_map(type_map) + def initialize_type_map(type_map, oids = nil) if supports_ranges? - result = execute(<<-SQL, 'SCHEMA') + query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype FROM pg_type as t LEFT JOIN pg_range as r ON oid = rngtypid SQL else - result = execute(<<-SQL, 'SCHEMA') + query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype FROM pg_type as t SQL end - ranges, nodes = result.partition { |row| row['typtype'] == 'r' } - enums, nodes = nodes.partition { |row| row['typtype'] == 'e' } - domains, nodes = nodes.partition { |row| row['typtype'] == 'd' } - arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' } - leaves, nodes = nodes.partition { |row| row['typelem'] == '0' } - - # populate the enum types - enums.each do |row| - type_map[row['oid'].to_i] = OID::Enum.new - end - - # populate the base types - leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row| - type_map[row['oid'].to_i] = OID::NAMES[row['typname']] - end - - records_by_oid = result.group_by { |row| row['oid'] } - - # populate composite types - nodes.each do |row| - add_oid row, records_by_oid, type_map - end - # populate array types - arrays.find_all { |row| type_map.key? row['typelem'].to_i }.each do |row| - array = OID::Array.new type_map[row['typelem'].to_i] - type_map[row['oid'].to_i] = array + if oids + query += "WHERE t.oid::integer IN (%s)" % oids.join(", ") end - # populate range types - ranges.find_all { |row| type_map.key? row['rngsubtype'].to_i }.each do |row| - subtype = type_map[row['rngsubtype'].to_i] - range = OID::Range.new subtype - type_map[row['oid'].to_i] = range - end - - # populate domain types - domains.each do |row| - base_type_oid = row["typbasetype"].to_i - if base_type = type_map[base_type_oid] - type_map[row['oid'].to_i] = base_type - else - warn "unknown base type (OID: #{base_type_oid}) for domain #{row["typname"]}." - end - end + initializer = OID::TypeMapInitializer.new(type_map) + records = execute(query, 'SCHEMA') + initializer.run(records) end FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: + def execute_and_clear(sql, name, binds) + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + ret = yield result + result.clear + ret + end + def exec_no_cache(sql, name, binds) log(sql, name, binds) { @connection.async_exec(sql) } end @@ -704,11 +658,6 @@ module ActiveRecord @statements[sql_key] end - # The internal PostgreSQL identifier of the money data type. - MONEY_COLUMN_TYPE_OID = 790 #:nodoc: - # The internal PostgreSQL identifier of the BYTEA data type. - BYTEA_COLUMN_TYPE_OID = 17 #:nodoc: - # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect @@ -799,7 +748,7 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) #:nodoc: + def column_definitions(table_name) # :nodoc: exec_query(<<-end_sql, 'SCHEMA').rows SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod @@ -811,7 +760,7 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) + def extract_pg_identifier_from_name(name) # :nodoc: match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) if match_data @@ -821,12 +770,12 @@ module ActiveRecord end end - def extract_table_ref_from_insert_sql(sql) + def extract_table_ref_from_insert_sql(sql) # :nodoc: sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end - def create_table_definition(name, temporary, options, as = nil) + def create_table_definition(name, temporary, options, as = nil) # :nodoc: TableDefinition.new native_database_types, name, temporary, options, as end end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index cd1f7a16c6..a5e2619cb8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -30,7 +30,7 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] - ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") raise ActiveRecord::NoDatabaseError.new(error.message, error) @@ -41,14 +41,12 @@ module ActiveRecord end module ConnectionAdapters #:nodoc: - class SQLite3Column < Column #:nodoc: - class << self - def binary_to_string(value) - if value.encoding != Encoding::ASCII_8BIT - value = value.force_encoding(Encoding::ASCII_8BIT) - end - value + class SQLite3Binary < Type::Binary # :nodoc: + def cast_value(value) + if value.encoding != Encoding::ASCII_8BIT + value = value.force_encoding(Encoding::ASCII_8BIT) end + value end end @@ -69,7 +67,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, - timestamp: { name: "datetime" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, @@ -123,11 +120,7 @@ module ActiveRecord end end - class BindSubstitution < Arel::Visitors::SQLite # :nodoc: - include Arel::Visitors::BindVisitor - end - - def initialize(connection, logger, config) + def initialize(connection, logger, connection_options, config) super(connection, logger) @active = nil @@ -135,11 +128,12 @@ module ActiveRecord self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) @config = config + @visitor = Arel::Visitors::SQLite.new self + if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true - @visitor = Arel::Visitors::SQLite.new self else - @visitor = unprepared_visitor + @prepared_statements = false end end @@ -273,7 +267,7 @@ module ActiveRecord def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) + ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) end class ExplainPrettyPrinter @@ -299,9 +293,12 @@ module ActiveRecord # Don't cache statements if they are not prepared if without_prepared_statement?(binds) stmt = @connection.prepare(sql) - cols = stmt.columns - records = stmt.to_a - stmt.close + begin + cols = stmt.columns + records = stmt.to_a + ensure + stmt.close + end stmt = records else cache = @statements[sql] ||= { @@ -394,7 +391,9 @@ module ActiveRecord field["dflt_value"] = $1.gsub('""', '"') end - SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0) + sql_type = field['type'] + cast_type = lookup_cast_type(sql_type) + Column.new(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -495,14 +494,18 @@ 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}) - rename_column_indexes(table_name, column_name, new_column_name) + column = column_for(table_name, column_name) + alter_table(table_name, rename: {column.name => new_column_name.to_s}) + rename_column_indexes(table_name, column.name, new_column_name) end protected + + def initialize_type_map(m) + super + m.register_type(/binary/i, SQLite3Binary.new) + end + def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) end diff --git a/activerecord/lib/active_record/connection_adapters/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb new file mode 100644 index 0000000000..763176cb2b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type.rb @@ -0,0 +1,32 @@ +require 'active_record/connection_adapters/type/numeric' +require 'active_record/connection_adapters/type/time_value' +require 'active_record/connection_adapters/type/value' + +require 'active_record/connection_adapters/type/binary' +require 'active_record/connection_adapters/type/boolean' +require 'active_record/connection_adapters/type/date' +require 'active_record/connection_adapters/type/date_time' +require 'active_record/connection_adapters/type/decimal' +require 'active_record/connection_adapters/type/float' +require 'active_record/connection_adapters/type/integer' +require 'active_record/connection_adapters/type/string' +require 'active_record/connection_adapters/type/text' +require 'active_record/connection_adapters/type/time' + +require 'active_record/connection_adapters/type/type_map' +require 'active_record/connection_adapters/type/hash_lookup_type_map' + +module ActiveRecord + module ConnectionAdapters + module Type # :nodoc: + class << self + def extract_scale(sql_type) + case sql_type + when /^(numeric|decimal|number)\((\d+)\)/i then 0 + when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb new file mode 100644 index 0000000000..4b2d1a66e0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def klass + ::String + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb new file mode 100644 index 0000000000..2337bdd563 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Boolean < Value # :nodoc: + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + Column::TRUE_VALUES.include?(value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb new file mode 100644 index 0000000000..1e7205fd0b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -0,0 +1,44 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + end + + private + + def cast_value(value) + if value.is_a?(::String) + return if value.empty? + fast_string_to_date(value) || fallback_string_to_date(value) + elsif value.respond_to?(:to_date) + value.to_date + else + value + end + end + + def fast_string_to_date(string) + if string =~ Column::Format::ISO_DATE + new_date $1.to_i, $2.to_i, $3.to_i + end + end + + def fallback_string_to_date(string) + new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday)) + end + + def new_date(year, mon, mday) + if year && year != 0 + ::Date.new(year, mon, mday) rescue nil + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb new file mode 100644 index 0000000000..c34f4c5a53 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class DateTime < Value # :nodoc: + include TimeValue + + def type + :datetime + end + + private + + def cast_value(string) + return string unless string.is_a?(::String) + return if string.empty? + + fast_string_to_time(string) || fallback_string_to_time(string) + end + + # '0.123456' -> 123456 + # '1.123456' -> 123456 + def microseconds(time) + time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0 + end + + def fallback_string_to_time(string) + time_hash = ::Date._parse(string) + time_hash[:sec_fraction] = microseconds(time_hash) + + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb new file mode 100644 index 0000000000..ac5af4b963 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def klass + ::BigDecimal + end + + private + + def cast_value(value) + if value.respond_to?(:to_d) + value.to_d + else + value.to_s.to_d + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb new file mode 100644 index 0000000000..51cfa5d86a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + def klass + ::Float + end + + private + + def cast_value(value) + value.to_f + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..8503d3ea1b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/hash_lookup_type_map.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type) + @mapping.fetch(type, proc { default_value }).call(type) + end + + def fetch(type, &block) + @mapping.fetch(type, block).call(type) + end + + def alias_type(type, alias_type) + register_type(type) { lookup(alias_type) } + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb new file mode 100644 index 0000000000..8f3469434c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + def klass + ::Fixnum + end + + private + + def cast_value(value) + case value + when true then 1 + when false then 0 + else value.to_i rescue nil + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/numeric.rb b/activerecord/lib/active_record/connection_adapters/type/numeric.rb new file mode 100644 index 0000000000..a3379831cb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/numeric.rb @@ -0,0 +1,20 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module Numeric # :nodoc: + def number? + true + end + + def type_cast_for_write(value) + case value + when true then 1 + when false then 0 + when ::String then value.presence + else super + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb new file mode 100644 index 0000000000..55f0e1ee1c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -0,0 +1,29 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class String < Value # :nodoc: + def type + :string + end + + def text? + true + end + + def klass + ::String + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else value.to_s + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/text.rb b/activerecord/lib/active_record/connection_adapters/type/text.rb new file mode 100644 index 0000000000..ee5842a3fc --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/text.rb @@ -0,0 +1,13 @@ +require 'active_record/connection_adapters/type/string' + +module ActiveRecord + module ConnectionAdapters + module Type + class Text < String # :nodoc: + def type + :text + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb new file mode 100644 index 0000000000..4dd201e3fe --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Time < Value # :nodoc: + include TimeValue + + def type + :time + end + + private + + def cast_value(value) + return value unless value.is_a?(::String) + return if value.empty? + + dummy_time_value = "2000-01-01 #{value}" + + fast_string_to_time(dummy_time_value) || begin + time_hash = ::Date._parse(dummy_time_value) + return if time_hash[:hour].nil? + new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction)) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/time_value.rb b/activerecord/lib/active_record/connection_adapters/type/time_value.rb new file mode 100644 index 0000000000..e9ca4adeda --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/time_value.rb @@ -0,0 +1,36 @@ +module ActiveRecord + module ConnectionAdapters + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + private + + def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil) + # Treat 0000-00-00 00:00:00 as nil. + return if year.nil? || (year == 0 && mon == 0 && mday == 0) + + if offset + time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil + return unless time + + time -= offset + Base.default_timezone == :utc ? time : time.getlocal + else + ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil + end + end + + # Doesn't handle time zones. + def fast_string_to_time(string) + if string =~ Column::Format::ISO_DATETIME + microsec = ($7.to_r * 1_000_000).to_i + new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/type_map.rb b/activerecord/lib/active_record/connection_adapters/type/type_map.rb new file mode 100644 index 0000000000..d89171a820 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/type_map.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + end + + def lookup(lookup_key) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key) + else + default_value + end + end + + def register_type(key, value = nil, &block) + raise ::ArgumentError unless value || block + + if block + @mapping[key] = block + else + @mapping[key] = proc { value } + end + end + + def alias_type(key, target_key) + register_type(key) do |sql_type| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}") + end + end + + def clear + @mapping.clear + end + + private + + def default_value + @default_value ||= Value.new + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb new file mode 100644 index 0000000000..289c27f6d4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module Type + class Value # :nodoc: + def type; end + + def extract_scale(sql_type) + Type.extract_scale(sql_type) + end + + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_write(value) + value + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def klass + ::Object + end + + private + + def cast_value(value) + value + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index bbb866cedf..31e7390bf7 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -58,9 +58,9 @@ module ActiveRecord end class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations, url = ENV['DATABASE_URL']) + def initialize(raw_configurations) @raw_config = raw_configurations.dup - @url = url + @env = DEFAULT_ENV.call.to_s end # Returns fully resolved connection hashes. @@ -71,33 +71,10 @@ module ActiveRecord private def config - if @url - raw_merged_into_default - else - @raw_config - end - end - - def raw_merged_into_default - default = default_url_hash - - @raw_config.each do |env, values| - default[env] = values || {} - default[env].merge!("url" => @url) { |h, v1, v2| v1 || v2 } if default[env].is_a?(Hash) - end - default - end - - # When the raw configuration is not present and ENV['DATABASE_URL'] - # is available we return a hash with the connection information in - # the connection URL. This hash responds to any string key with - # resolved connection information. - def default_url_hash - Hash.new do |hash, key| - hash[key] = if key.is_a? String - ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(@url).to_hash - else - nil + @raw_config.dup.tap do |cfg| + if url = ENV['DATABASE_URL'] + cfg[@env] ||= {} + cfg[@env]["url"] ||= url end end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 4e53f66005..4571cc0786 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -94,6 +94,7 @@ module ActiveRecord end class_attribute :default_connection_handler, instance_writer: false + class_attribute :find_by_statement_cache def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler @@ -107,6 +108,71 @@ module ActiveRecord end module ClassMethods + def initialize_find_by_cache + self.find_by_statement_cache = {}.extend(Mutex_m) + end + + def inherited(child_class) + child_class.initialize_find_by_cache + super + end + + def find(*ids) + # We don't have cache keys for this stuff yet + return super unless ids.length == 1 + return super if block_given? || + primary_key.nil? || + default_scopes.any? || + columns_hash.include?(inheritance_column) || + ids.first.kind_of?(Array) + + id = ids.first + if ActiveRecord::Base === id + id = id.id + ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ + "Please pass the id of the object by calling `.id`" + end + key = primary_key + + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + where(key => params.bind).limit(1) + } + } + record = s.execute([id], self, connection).first + unless record + raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" + end + record + end + + def find_by(*args) + return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any? + + hash = args.first + + return super if hash.values.any? { |v| + v.nil? || Array === v || Hash === v + } + + key = hash.keys + + klass = self + s = find_by_statement_cache[key] || find_by_statement_cache.synchronize { + find_by_statement_cache[key] ||= StatementCache.create(connection) { |params| + wheres = key.each_with_object({}) { |param,o| + o[param] = params.bind + } + klass.where(wheres).limit(1) + } + } + begin + s.execute(hash.values, self, connection).first + rescue TypeError => e + raise ActiveRecord::StatementInvalid.new(e.message, e) + end + end + def initialize_generated_modules super @@ -267,6 +333,7 @@ module ActiveRecord @attributes_cache = {} @new_record = true + @destroyed = false super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index dcbdf75627..71e176a328 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -11,7 +11,7 @@ module ActiveRecord # ==== Parameters # # * +id+ - The id of the object you wish to reset a counter on. - # * +counters+ - One or more association counters to reset + # * +counters+ - One or more association counters to reset. Association name or counter name can be given. # # ==== Examples # @@ -19,9 +19,14 @@ module ActiveRecord # Post.reset_counters(1, :comments) def reset_counters(id, *counters) object = find(id) - counters.each do |association| - has_many_association = reflect_on_association(association.to_sym) - raise ArgumentError, "'#{self.name}' has no association called '#{association}'" unless has_many_association + counters.each do |counter_association| + has_many_association = reflect_on_association(counter_association.to_sym) + unless has_many_association + has_many = reflect_on_all_associations(:has_many) + has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } + counter_association = has_many_association.plural_name if has_many_association + end + raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection has_many_association = has_many_association.through_reflection @@ -34,7 +39,7 @@ module ActiveRecord counter_name = reflection.counter_cache_column stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(association).count + arel_table[counter_name] => object.send(counter_association).count }, primary_key) connection.update stmt end @@ -118,5 +123,54 @@ module ActiveRecord update_counters(id, counter_name => -1) end end + + protected + + def actually_destroyed? + @_actually_destroyed + end + + def clear_destroy_state + @_actually_destroyed = nil + end + + private + + def _create_record(*) + id = super + + each_counter_cached_associations do |association| + if send(association.reflection.name) + association.increment_counters + @_after_create_counter_called = true + end + end + + id + end + + def destroy_row + affected_rows = super + + if affected_rows > 0 + each_counter_cached_associations do |association| + foreign_key = association.reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + if send(association.reflection.name) + association.decrement_counters + end + end + end + end + + affected_rows + end + + def each_counter_cached_associations + reflections.each do |name, reflection| + yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + end + end + end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 4aa323fb00..c7ec093824 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/deep_dup' + module ActiveRecord # Declare an enum attribute where the values map to integers in the database, # but can be queried by name. Example: @@ -25,8 +27,10 @@ module ActiveRecord # conversation.status # => nil # # Scopes based on the allowed values of the enum field will be provided - # as well. With the above example, it will create an +active+ and +archived+ - # scope. + # as well. With the above example: + # + # Conversation.active + # Conversation.archived # # You can set the default value from the database declaration, like: # @@ -65,10 +69,14 @@ module ActiveRecord # # Where conditions on an enum attribute must use the ordinal value of an enum. module Enum - DEFINED_ENUMS = {} # :nodoc: + def self.extended(base) + base.class_attribute(:defined_enums) + base.defined_enums = {} + end - def enum_mapping_for(attr_name) # :nodoc: - DEFINED_ENUMS[attr_name.to_s] + def inherited(base) + base.defined_enums = defined_enums.deep_dup + super end def enum(definitions) @@ -122,9 +130,8 @@ module ActiveRecord klass.send(:detect_enum_conflict!, name, value, true) klass.scope value, -> { klass.where name => i } end - - DEFINED_ENUMS[name.to_s] = enum_values end + defined_enums[name.to_s] = enum_values end end @@ -134,7 +141,7 @@ module ActiveRecord mod = Module.new do private def save_changed_attribute(attr_name, value) - if (mapping = self.class.enum_mapping_for(attr_name)) + if (mapping = self.class.defined_enums[attr_name.to_s]) if attribute_changed?(attr_name) old = changed_attributes[attr_name] diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6f134bbef8..47d32fae05 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,6 +2,7 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' +require 'active_support/core_ext/securerandom' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -550,9 +551,13 @@ module ActiveRecord end # Returns a consistent, platform-independent identifier for +label+. - # Identifiers are positive integers less than 2^30. - def self.identify(label) - Zlib.crc32(label.to_s) % MAX_ID + # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. + def self.identify(label, column_type = :integer) + if column_type == :uuid + SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s) + else + Zlib.crc32(label.to_s) % MAX_ID + end end # Superclass for the evaluation contexts used by ERB fixtures. @@ -633,7 +638,7 @@ module ActiveRecord # generate a primary key if necessary if has_primary_key_column? && !row.include?(primary_key_name) - row[primary_key_name] = ActiveRecord::FixtureSet.identify(label) + row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type) end # If STI is used, find the correct subclass for association reflection @@ -656,7 +661,8 @@ module ActiveRecord row[association.foreign_type] = $1 end - row[fk_name] = ActiveRecord::FixtureSet.identify(value) + fk_type = association.active_record.columns_hash[association.foreign_key].type + row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many if association.options[:through] @@ -683,6 +689,10 @@ module ActiveRecord def name @association.name end + + def primary_key_type + @association.klass.column_types[@association.klass.primary_key].type + end end class HasManyThroughProxy < ReflectionProxy # :nodoc: @@ -700,17 +710,22 @@ module ActiveRecord @primary_key_name ||= model_class && model_class.primary_key end + def primary_key_type + @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type + end + def add_join_records(rows, row, association) # This is the case when the join table has no fixtures file if (targets = row.delete(association.name.to_s)) - table_name = association.join_table - lhs_key = association.lhs_key - rhs_key = association.rhs_key + table_name = association.join_table + column_type = association.primary_key_type + lhs_key = association.lhs_key + rhs_key = association.rhs_key targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/) rows[table_name].concat targets.map { |target| { lhs_key => row[primary_key_name], - rhs_key => ActiveRecord::FixtureSet.identify(target) } + rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) } } end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 654ef21b07..eb64d197f0 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -25,7 +25,7 @@ module ActiveRecord if column.binary? # This specifically deals with the PG adapter that casts bytea columns into a Hash. value = value[:value] if value.is_a?(Hash) - value = "<#{value.bytesize} bytes of binary data>" + value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>" end [column.name, value] diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b6b02322d7..8fe32bcb6c 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -195,7 +195,7 @@ module ActiveRecord # == Database support # # Migrations are currently supported in MySQL, PostgreSQL, SQLite, - # SQL Server, Sybase, and Oracle (all supported databases except DB2). + # SQL Server, and Oracle (all supported databases except DB2). # # == More examples # @@ -640,7 +640,7 @@ module ActiveRecord say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert - unless arguments.empty? || method == :execute + unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index dc5ff02882..aa1166750f 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -29,6 +29,10 @@ module ActiveRecord # :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. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. class_attribute :table_name_suffix, instance_writer: false self.table_name_suffix = "" @@ -153,6 +157,10 @@ module ActiveRecord (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end + def full_table_name_suffix #:nodoc: + (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + end + # Defines the name of the table column which will store the class name on single-table # inheritance situations. # @@ -190,7 +198,7 @@ module ActiveRecord # 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, + # If a sequence name is not explicitly set when using Oracle, # it will default to the commonly used pattern of: #{table_name}_seq # # If a sequence name is not explicitly set when using PostgreSQL, it @@ -337,7 +345,8 @@ module ActiveRecord contained = contained.singularize if parent.pluralize_table_names contained += '_' end - "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}" + + "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. base.table_name diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 9d92e747d4..29ed499b1b 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -485,10 +485,10 @@ module ActiveRecord end # Takes in a limit and checks if the attributes_collection has too many - # records. The method will take limits in the form of symbols, procs, and - # number-like objects (anything that can be compared with an integer). + # records. It accepts limit in the form of symbol, proc, or + # number-like object (anything that can be compared with an integer). # - # Will raise an TooManyRecords error if the attributes_collection is + # Raises TooManyRecords error if the attributes_collection is # larger than the limit. def check_record_limit!(limit, attributes_collection) if limit @@ -516,10 +516,10 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) + ConnectionAdapters::Type::Boolean.new.type_cast(hash['_destroy']) end - # Determines if a new record should be build by checking for + # Determines if a new record should be rejected by checking # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 5b255c3fe5..05d0c41678 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -43,7 +43,7 @@ module ActiveRecord end def count(*) - 0 + calculate :count, nil end def sum(*) @@ -54,7 +54,7 @@ module ActiveRecord # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. if operation == :count - 0 + group_values.any? ? Hash.new : 0 else nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 13d7432773..b74e340b3e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -37,7 +37,7 @@ module ActiveRecord end # Given an attributes hash, +instantiate+ returns a new instance of - # the appropriate class. + # the appropriate class. Accepts only keys as strings. # # For example, +Post.all+ may return Comments, Messages, and Emails # by storing the record's subclass in a +type+ attribute. By calling @@ -46,10 +46,10 @@ module ActiveRecord # # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see # how this "single-table" inheritance mapping is implemented. - def instantiate(record, column_types = {}) - klass = discriminate_class_for_record(record) + def instantiate(attributes, column_types = {}) + klass = discriminate_class_for_record(attributes) column_types = klass.decorate_columns(column_types.dup) - klass.allocate.init_with('attributes' => record, 'column_types' => column_types) + klass.allocate.init_with('attributes' => attributes, 'column_types' => column_types) end private diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6b0459ea37..9e8e5fe94b 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -83,8 +83,7 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) - puts 'Schema migrations table does not exist yet.' - next # means "return" for rake task + abort 'Schema migrations table does not exist yet.' end db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") db_list.map! { |version| "%.3d" % version } @@ -278,7 +277,7 @@ db_namespace = namespace :db do db_namespace['structure:dump'].reenable end - # desc "Recreate the databases from the structure.sql file" + desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 03b5bdc46c..0eec6774a0 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,3 +1,5 @@ +require 'thread' + module ActiveRecord # = Active Record Reflection module Reflection # :nodoc: @@ -22,11 +24,11 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) - ar.reflections = ar.reflections.merge(name => reflection) + ar.reflections = ar.reflections.merge(name.to_s => reflection) end def self.add_aggregate_reflection(ar, name, reflection) - ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection) + ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end # \Reflection enables to interrogate Active Record classes and objects @@ -48,7 +50,7 @@ module ActiveRecord # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection # def reflect_on_aggregation(aggregation) - aggregate_reflections[aggregation] + aggregate_reflections[aggregation.to_s] end # Returns an array of AssociationReflection objects for all the @@ -72,7 +74,7 @@ module ActiveRecord # Invoice.reflect_on_association(:line_items).macro # returns :has_many # def reflect_on_association(association) - reflections[association] + reflections[association.to_s] end # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. @@ -199,6 +201,18 @@ module ActiveRecord @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) + @association_scope_cache = {} + @scope_lock = Mutex.new + end + + def association_scope_cache(conn, owner) + key = conn.prepared_statements + if options[:polymorphic] + key = [key, owner.read_attribute(@foreign_type)] + end + @association_scope_cache[key] ||= @scope_lock.synchronize { + @association_scope_cache[key] ||= yield + } end # Returns a new, unsaved instance of the associated class. +attributes+ will @@ -264,6 +278,25 @@ module ActiveRecord end end + def check_preloadable! + return unless scope + + if scope.arity > 0 + ActiveSupport::Deprecation.warn <<-WARNING +The association scope '#{name}' is instance dependent (the scope block takes an argument). +Preloading happens before the individual instances are created. This means that there is no instance +being passed to the association scope. This will most likely result in broken or incorrect behavior. +Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future. + WARNING + end + end + alias :check_eager_loadable! :check_preloadable! + + def join_id_for(owner) #:nodoc: + key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key + owner[key] + end + def through_reflection nil end @@ -449,9 +482,9 @@ module ActiveRecord end def derive_class_name - class_name = name.to_s.camelize + class_name = name.to_s class_name = class_name.singularize if collection? - class_name + class_name.camelize end def derive_foreign_key @@ -617,7 +650,7 @@ module ActiveRecord # # => [:tag, :tags] # def source_reflection_names - (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq + options[:source] ? [options[:source]] : [name.to_s.singularize, name].uniq end def source_reflection_name # :nodoc: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 4d37ac6e2b..d92ff781ee 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +require 'arel/collectors/bind' module ActiveRecord # = Active Record Relation @@ -11,6 +12,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering, :reverse_order, :distinct, :create_with, :uniq] + INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS @@ -223,6 +225,7 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain + #TODO: Fix for binds. exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -238,7 +241,7 @@ module ActiveRecord # Returns size of the records. def size - loaded? ? @records.length : count + loaded? ? @records.length : count(:all) end # Returns true if there are no records. @@ -248,8 +251,7 @@ module ActiveRecord if limit_value == 0 true else - # FIXME: This count is not compatible with #select('authors.*') or other select narrows - c = count + c = count(:all) c.respond_to?(:zero?) ? c.zero? : c.empty? end end @@ -324,7 +326,8 @@ module ActiveRecord stmt.wheres = arel.constraints end - @klass.connection.update stmt, 'SQL', bind_values + bvs = bind_values + arel.bind_values + @klass.connection.update stmt, 'SQL', bvs end # Updates an object (or multiple objects) and saves it to the database, if validations pass. @@ -428,12 +431,21 @@ module ActiveRecord # If you need to destroy dependent associations or call your <tt>before_*</tt> or # +after_destroy+ callbacks, use the +destroy_all+ method instead. # - # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error: + # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error: # # Post.limit(100).delete_all - # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope + # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit def delete_all(conditions = nil) - raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value + invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| + if MULTI_VALUE_METHODS.include?(method) + send("#{method}_values").any? + else + send("#{method}_value") + end + } + if invalid_methods.any? + raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") + end if conditions where(conditions).delete_all @@ -517,11 +529,11 @@ module ActiveRecord find_with_associations { |rel| relation = rel } end - ast = relation.arel.ast - binds = relation.bind_values.dup - visitor.accept(ast) do - connection.quote(*binds.shift.reverse) - end + arel = relation.arel + binds = (arel.bind_values + relation.bind_values).dup + binds.map! { |bv| connection.quote(*bv.reverse) } + collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new) + collect.substitute_binds(binds).join end end @@ -529,16 +541,22 @@ module ActiveRecord # # User.where(name: 'Oscar').where_values_hash # # => {name: "Oscar"} - def where_values_hash + def where_values_hash(relation_table_name = table_name) equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node| - node.left.relation.name == table_name + node.left.relation.name == relation_table_name } binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] Hash[equalities.map { |where| name = where.left.name - [name, binds.fetch(name.to_s) { where.right }] + [name, binds.fetch(name.to_s) { + case where.right + when Array then where.right.map(&:val) + else + where.right.val + end + }] }] end @@ -570,6 +588,8 @@ module ActiveRecord # Compares two relations for equality. def ==(other) case other + when Associations::CollectionProxy, AssociationRelation + self == other.to_a when Relation other.to_sql == to_sql when Array @@ -600,7 +620,7 @@ module ActiveRecord private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values) + @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values) preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 45ffb99868..56cf9bcd27 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -19,6 +19,14 @@ module ActiveRecord # # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } + # + # If +count+ is used with +select+, it will count the selected columns: + # + # Person.select(:age).count + # # => counts the number of different age values + # + # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ + # between databases. In invalid cases, an error from the databsae is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -188,7 +196,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end def perform_calculation(operation, column_name, options = {}) @@ -231,15 +239,18 @@ module ActiveRecord def execute_simple_calculation(operation, column_name, distinct) #:nodoc: # Postgresql doesn't like ORDER BY when there are no GROUP BY - relation = reorder(nil) + relation = unscope(:order) column_alias = column_name + bind_values = nil + if operation == "count" && (relation.limit_value || relation.offset_value) # Shortcut when limit is zero. return 0 if relation.limit_value == 0 query_builder = build_count_subquery(relation, column_name, distinct) + bind_values = query_builder.bind_values + relation.bind_values else column = aggregate_column(column_name) @@ -249,9 +260,10 @@ module ActiveRecord relation.select_values = [select_value] query_builder = relation.arel + bind_values = query_builder.bind_values + relation.bind_values end - result = @klass.connection.select_all(query_builder, nil, relation.bind_values) + result = @klass.connection.select_all(query_builder, nil, bind_values) row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do @@ -385,9 +397,11 @@ module ActiveRecord aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias) relation.select_values = [aliased_column] - subquery = relation.arel.as(subquery_alias) + arel = relation.arel + subquery = arel.as(subquery_alias) sm = Arel::SelectManager.new relation.engine + sm.bind_values = arel.bind_values select_value = operation_over_aggregate_column(column_alias, 'count', distinct) sm.project(select_value).from(subquery) end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 21beed332f..50f4d5c7ab 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -40,10 +40,10 @@ module ActiveRecord BLACKLISTED_ARRAY_METHODS = [ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :compact + :keep_if, :pop, :shift, :delete_at, :compact, :select! ].to_set # :nodoc: - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c2b9dc08fe..db32ae12a8 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -129,9 +129,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_value, limit) + find_nth_with_limit(offset_index, limit) else - find_nth(:first, offset_value) + find_nth(0, offset_index) end end @@ -181,7 +181,7 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(:second, offset_value ? offset_value + 1 : 1) + find_nth(1, offset_index) end # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -197,7 +197,7 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(:third, offset_value ? offset_value + 2 : 2) + find_nth(2, offset_index) end # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -213,7 +213,7 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(:fourth, offset_value ? offset_value + 3 : 3) + find_nth(3, offset_index) end # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -229,7 +229,7 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(:fifth, offset_value ? offset_value + 4 : 4) + find_nth(4, offset_index) end # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -242,10 +242,10 @@ module ActiveRecord # If no order is defined it will order by primary key. # # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people - # Person.offset(3).forty_two # returns the fifth object from OFFSET 3 (which is OFFSET 44) + # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(:forty_two, offset_value ? offset_value + 41 : 41) + find_nth(41, offset_index) end # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record @@ -299,11 +299,8 @@ module ActiveRecord when Array, Hash relation = relation.where(conditions) else - if conditions != :none - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, conditions]] + unless conditions == :none + relation = where(primary_key => conditions) end end @@ -334,6 +331,10 @@ module ActiveRecord private + def offset_index + offset_value || 0 + end + def find_with_associations join_dependency = construct_join_dependency @@ -347,7 +348,8 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup) + arel = relation.arel + rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) join_dependency.instantiate(rows, aliases) end end @@ -468,20 +470,24 @@ module ActiveRecord end end - def find_nth(ordinal, offset) + def find_nth(index, offset) if loaded? - @records.send(ordinal) + @records[index] else + offset += index @offsets[offset] ||= find_nth_with_limit(offset, 1).first end end def find_nth_with_limit(offset, limit) - if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a - else - limit(limit).offset(offset).to_a - end + relation = if order_values.empty? && primary_key + order(arel_table[primary_key].asc) + else + self + end + + relation = relation.offset(offset) unless offset.zero? + relation.limit(limit).to_a end def find_last diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 182b9ed89c..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -30,6 +30,8 @@ module ActiveRecord else other.joins!(*v) end + elsif k == :select + other._select!(v) else other.send("#{k}!", v) end @@ -62,7 +64,13 @@ module ActiveRecord # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values # don't fall through the cracks. - relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value) + unless value.nil? || (value.blank? && false != value) + if name == :select + relation._select!(*value) + else + relation.send("#{name}!", *value) + end + end end merge_multi_values @@ -139,7 +147,6 @@ module ActiveRecord def merge_single_values relation.from_value = values[:from] unless relation.from_value relation.lock_value = values[:lock] unless relation.lock_value - relation.reverse_order_value = values[:reverse_order] unless values[:create_with].blank? relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) @@ -149,7 +156,7 @@ module ActiveRecord def filter_binds(lhs_binds, removed_wheres) return lhs_binds if removed_wheres.empty? - set = Set.new removed_wheres.map { |x| x.left.name } + set = Set.new removed_wheres.map { |x| x.left.name.to_s } lhs_binds.dup.delete_if { |col,_| set.include? col.name } end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 1252af7635..d40f276968 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -113,13 +113,14 @@ module ActiveRecord register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new) - private - def self.build(attribute, value) - handler_for(value).call(attribute, value) - end + def self.build(attribute, value) + handler_for(value).call(attribute, value) + end + private_class_method :build - def self.handler_for(object) - @handlers.detect { |klass, _| klass === object }.last - end + def self.handler_for(object) + @handlers.detect { |klass, _| klass === object }.last + end + private_class_method :handler_for end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 0213bca981..416f2305d2 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -64,6 +64,7 @@ module ActiveRecord # def #{name}_values=(values) # def select_values=(values) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = values # @values[:select] = values end # end CODE @@ -81,11 +82,22 @@ module ActiveRecord class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_value=(value) # def readonly_value=(value) raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded + check_cached_relation @values[:#{name}] = value # @values[:readonly] = value end # end CODE end + def check_cached_relation # :nodoc: + if defined?(@arel) && @arel + @arel = nil + ActiveSupport::Deprecation.warn <<-WARNING +Modifying already cached Relation. The cache will be reset. +Use a cloned Relation to prevent this warning. +WARNING + end + end + def create_with_value # :nodoc: @values[:create_with] || {} end @@ -235,11 +247,11 @@ module ActiveRecord to_a.select { |*block_args| yield(*block_args) } else raise ArgumentError, 'Call this with at least one field' if fields.empty? - spawn.select!(*fields) + spawn._select!(*fields) end end - def select!(*fields) # :nodoc: + def _select!(*fields) # :nodoc: fields.flatten! fields.map! do |field| klass.attribute_alias?(field) ? klass.attribute_alias(field) : field @@ -825,7 +837,9 @@ module ActiveRecord end def reverse_order! # :nodoc: - self.reverse_order_value = !reverse_order_value + orders = order_values.uniq + orders.reject!(&:blank?) + self.order_values = reverse_sql_order(orders) self end @@ -841,7 +855,7 @@ module ActiveRecord build_joins(arel, joins_values.flatten) unless joins_values.empty? - collapse_wheres(arel, (where_values - ['']).uniq) + collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty? @@ -858,6 +872,15 @@ module ActiveRecord arel.from(build_from) if from_value arel.lock(lock_value) if lock_value + # Reorder bind indexes if joins produced bind values + if arel.bind_values.any? + bvs = arel.bind_values + bind_values + arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| + column = bvs[i].first + bp.replace connection.substitute_at(column, i) + end + end + arel end @@ -871,8 +894,9 @@ module ActiveRecord case scope when :order - self.reverse_order_value = false result = [] + when :where + self.bind_values = [] else result = [] unless single_val_method end @@ -923,18 +947,16 @@ module ActiveRecord def build_where(opts, other = []) case opts when String, Array - #TODO: Remove duplication with: /activerecord/lib/active_record/sanitization.rb:113 - values = Hash === other.first ? other.first.values : other - - values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end - [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash opts = PredicateBuilder.resolve_column_aliases(klass, opts) attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts) + bv_len = bind_values.length + tmp_opts, bind_values = create_binds(opts, bv_len) + self.bind_values += bind_values + + attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) attributes.values.grep(ActiveRecord::Relation) do |rel| self.bind_values += rel.bind_values end @@ -945,6 +967,29 @@ module ActiveRecord end end + def create_binds(opts, idx) + bindable, non_binds = opts.partition do |column, value| + case value + when String, Integer, ActiveRecord::StatementCache::Substitute + @klass.columns_hash.include? column.to_s + else + false + end + end + + new_opts = {} + binds = [] + + bindable.each_with_index do |(column,value), index| + binds.push [@klass.columns_hash[column.to_s], value] + new_opts[column] = connection.substitute_at(column, index + idx) + end + + non_binds.each { |column,value| new_opts[column] = value } + + [new_opts, binds] + end + def build_from opts, name = from_value case opts @@ -986,9 +1031,12 @@ module ActiveRecord join_list ) - joins = join_dependency.join_constraints stashed_association_joins + join_infos = join_dependency.join_constraints stashed_association_joins - joins.each { |join| manager.from(join) } + join_infos.each do |info| + info.joins.each { |join| manager.from(join) } + manager.bind_values.concat info.binds + end manager.join_sources.concat(join_list) @@ -1031,7 +1079,6 @@ module ActiveRecord def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) - orders = reverse_sql_order(orders) if reverse_order_value arel.order(*orders) unless orders.empty? end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 2552cbd234..57d66bce4b 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -58,6 +58,9 @@ module ActiveRecord # Post.order('id asc').only(:where) # discards the order condition # Post.order('id asc').only(:where, :order) # uses the specified order def only(*onlies) + if onlies.any? { |o| o == :where } + onlies << :bind + end relation_with values.slice(*onlies) end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5a71c13d91..1aa93ffbb3 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -92,7 +92,7 @@ module ActiveRecord table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) PredicateBuilder.build_from_hash(self, attrs, table).map { |b| - connection.visitor.accept b + connection.visitor.compile b }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -107,6 +107,13 @@ module ActiveRecord end.join(', ') end + # Sanitizes a +string+ so that it is safe to use within a sql + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].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'" diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index dd4ee0c4a0..aece446384 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -14,13 +14,87 @@ module ActiveRecord # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. # Database is queried when +to_a+ is called on the relation. class StatementCache - def initialize - @relation = yield - raise ArgumentError.new("Statement cannot be nil") if @relation.nil? + class Substitute; end + + class Query + def initialize(sql) + @sql = sql + end + + def sql_for(binds, connection) + @sql + end + end + + class PartialQuery < Query + def initialize values + @values = values + @indexes = values.each_with_index.find_all { |thing,i| + Arel::Nodes::BindParam === thing + }.map(&:last) + end + + def sql_for(binds, connection) + val = @values.dup + binds = binds.dup + @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) } + val.join + end + end + + def self.query(visitor, ast) + Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value + end + + def self.partial_query(visitor, ast, collector) + collected = visitor.accept(ast, collector).value + PartialQuery.new collected + end + + class Params + def bind; Substitute.new; end end - def execute - @relation.dup.to_a + class BindMap + def initialize(bind_values) + @indexes = [] + @bind_values = bind_values + + bind_values.each_with_index do |(_, value), i| + if Substitute === value + @indexes << i + end + end + end + + def bind(values) + bvs = @bind_values.map { |pair| pair.dup } + @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } + bvs + end + end + + attr_reader :bind_map, :query_builder + + def self.create(connection, block = Proc.new) + relation = block.call Params.new + bind_map = BindMap.new relation.bind_values + query_builder = connection.cacheable_query relation.arel + new query_builder, bind_map + end + + def initialize(query_builder, bind_map) + @query_builder = query_builder + @bind_map = bind_map + end + + def execute(params, klass, connection) + bind_values = bind_map.bind params + + sql = query_builder.sql_for bind_values, connection + + klass.find_by_sql sql, bind_values end + alias :call :execute end end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 79a6ccbda0..7014bc6d45 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -66,8 +66,9 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :stored_attributes, instance_accessor: false - self.stored_attributes = {} + class << self + attr_accessor :local_stored_attributes + end end module ClassMethods @@ -93,9 +94,9 @@ module ActiveRecord # assign new store attribute and create new hash to ensure that each class in the hierarchy # has its own hash of stored attributes. - self.stored_attributes = {} if self.stored_attributes.blank? - self.stored_attributes[store_attribute] ||= [] - self.stored_attributes[store_attribute] |= keys + self.local_stored_attributes ||= {} + self.local_stored_attributes[store_attribute] ||= [] + self.local_stored_attributes[store_attribute] |= keys end def _store_accessors_module @@ -105,6 +106,14 @@ module ActiveRecord mod end end + + def stored_attributes + parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {} + if self.local_stored_attributes + parent.merge!(self.local_stored_attributes) { |k, a, b| a | b } + end + parent + end end protected diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 6ce0495f6f..168b338b97 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -28,7 +28,7 @@ module ActiveRecord # Example usage of +DatabaseTasks+ outside Rails could look as such: # # include ActiveRecord::Tasks - # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml')) + # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') # DatabaseTasks.db_dir = 'db' # # other settings... # diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 17f76b63b3..17d1ae1ba0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -1,5 +1,3 @@ -require 'thread' - module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions @@ -295,7 +293,7 @@ module ActiveRecord def committed! #:nodoc: run_callbacks :commit if destroyed? || persisted? ensure - @_start_transaction_state.clear + force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record @@ -328,7 +326,7 @@ module ActiveRecord begin status = yield rescue ActiveRecord::Rollback - @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 + clear_transaction_record_state status = nil end @@ -355,7 +353,12 @@ module ActiveRecord # Clear the new record state and id of a record. def clear_transaction_record_state #:nodoc: @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 - @_start_transaction_state.clear if @_start_transaction_state[:level] < 1 + force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 + end + + # Force to clear the transaction record state. + def force_clear_transaction_record_state #:nodoc: + @_start_transaction_state.clear end # Restore the new record state and id of a record that was previously saved by a call to save_record_state. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 71c71cb4b1..ee080451a9 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -93,7 +93,7 @@ module ActiveRecord end def map_enum_attribute(klass, attribute, value) - mapping = klass.enum_mapping_for(attribute.to_s) + mapping = klass.defined_enums[attribute.to_s] value = mapping[value] if value && mapping value end |