diff options
Diffstat (limited to 'activerecord/lib')
114 files changed, 2570 insertions, 2068 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/associations.rb b/activerecord/lib/active_record/associations.rb index 727ee5f65f..07dfc448e7 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -50,7 +50,7 @@ module ActiveRecord def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names - source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect } + source_associations = reflection.through_reflection.klass._reflections.keys super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -151,7 +151,7 @@ module ActiveRecord association = association_instance_get(name) if association.nil? - raise AssociationNotFoundError.new(self, name) unless reflection = self.class.reflect_on_association(name) + raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name) association = reflection.association_class.new(self, reflection) association_instance_set(name, association) end @@ -202,12 +202,13 @@ module ActiveRecord # For instance, +attributes+ and +connection+ would be bad choices for association names. # # == Auto-generated methods + # See also Instance Public methods below for more details. # # === Singular associations (one-to-one) # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other | X | X | X + # other(force_reload=false) | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -217,7 +218,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others | X | X | X + # others(force_reload=false) | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -1576,6 +1577,8 @@ module ActiveRecord scope = nil end + habtm_reflection = ActiveRecord::Reflection::AssociationReflection.new(:has_and_belongs_to_many, name, scope, options, self) + builder = Builder::HasAndBelongsToMany.new name, self, options join_model = builder.through_model @@ -1589,6 +1592,7 @@ module ActiveRecord Builder::HasMany.define_callbacks self, middle_reflection Reflection.add_reflection self, middle_reflection.name, middle_reflection + middle_reflection.parent_reflection = [name.to_s, habtm_reflection] include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 @@ -1609,6 +1613,7 @@ module ActiveRecord end has_many name, scope, hm_options, &extension + self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection] end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 85109aee6c..a6a1947148 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -32,8 +32,18 @@ module ActiveRecord join.left.downcase.scan( /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ ).size - else + elsif join.respond_to? :left join.left.table_name == name ? 1 : 0 + else + # this branch is reached by two tests: + # + # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37 + # with :posts + # + # activerecord/test/cases/associations/eager_test.rb:1133 + # with :comments + # + 0 end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 9ad2d2fb12..4a04303fb8 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -160,7 +160,7 @@ module ActiveRecord def marshal_load(data) reflection_name, ivars = data ivars.each { |name, val| instance_variable_set(name, val) } - @reflection = @owner.class.reflect_on_association(reflection_name) + @reflection = @owner.class._reflect_on_association(reflection_name) end def initialize_attributes(record) #:nodoc: diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 572f556999..31108cc1aa 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -106,7 +106,7 @@ module ActiveRecord table, foreign_table = tables.shift, tables.first if reflection.source_macro == :belongs_to - if reflection.options[:polymorphic] + if reflection.polymorphic? key = reflection.association_primary_key(assoc_klass) else key = reflection.association_primary_key diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 47cc1f4b34..3998aca23e 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -103,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..0ad5206980 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 @@ -60,13 +66,13 @@ module ActiveRecord::Associations::Builder def self.add_left_association(name, options) belongs_to name, options - self.left_reflection = reflect_on_association(name) + self.left_reflection = _reflect_on_association(name) end def self.add_right_association(name, options) rhs_name = name.to_s.singularize.to_sym belongs_to rhs_name, options - self.right_reflection = reflect_on_association(rhs_name) + self.right_reflection = _reflect_on_association(rhs_name) end } diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 48628230c7..c5f7bcae7d 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -194,7 +194,7 @@ module ActiveRecord options[:dependent] end - delete_records(:all, dependent).tap do + delete_or_nullify_all_records(dependent).tap do reset loaded! end @@ -244,6 +244,7 @@ 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] @@ -257,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 diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index aac85a36c8..2727e23870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -100,28 +100,33 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) - reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection| + reflection.klass._reflections.values.any? { |inverse_reflection| + :belongs_to == inverse_reflection.macro && inverse_reflection.counter_cache_column == counter_name } 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 aeb77e2753..954128064d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -116,7 +116,7 @@ module ActiveRecord end def target_reflection_has_associated_record? - !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?) + !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end def update_through_counter?(method) @@ -130,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) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5842be3a7b..35659766d3 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -207,7 +207,7 @@ module ActiveRecord end def find_reflection(klass, name) - klass.reflect_on_association(name) or + klass._reflect_on_association(name) or raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?" end @@ -217,7 +217,7 @@ module ActiveRecord reflection.check_validity! reflection.check_eager_loadable! - if reflection.options[:polymorphic] + if reflection.polymorphic? raise EagerLoadPolymorphicError.new(reflection) end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 20bd4947dc..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -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 @@ -142,6 +143,7 @@ module ActiveRecord def grouped_records(association, records) h = {} records.each do |record| + next unless record assoc = record.association(association) klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index bf461070e0..33c8619359 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_for_attribute(association_key_name).type + end + + def owner_key_type + @model.column_for_attribute(owner_key_name).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 diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f8a85b8a6f..fcf3b219d4 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -63,14 +63,13 @@ module ActiveRecord # Note: this does not capture all cases, for example it would be crazy to try to # properly support stale-checking for nested associations. def stale_state - if through_reflection.macro == :belongs_to + if through_reflection.belongs_to? owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s end end def foreign_key_present? - through_reflection.macro == :belongs_to && - !owner[through_reflection.foreign_key].nil? + through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil? end def ensure_mutable diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 87ecbe54f1..c4cf084a04 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -13,9 +13,9 @@ module ActiveRecord # exception is raised. # # cat = Cat.new(name: "Gorby", status: "yawning") - # cat.attributes # => { "name" => "Gorby", "status" => "yawning" } + # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil} # cat.assign_attributes(status: "sleeping") - # cat.attributes # => { "name" => "Gorby", "status" => "sleeping" } + # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil } # # New attributes will be persisted in the database when the object is saved. # @@ -149,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 7fd7accc6b..e626227e7e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -18,6 +18,8 @@ module ActiveRecord include TimeZoneConversion include Dirty include Serialization + + delegate :column_for_attribute, to: :class end AttrNames = Module.new { @@ -48,7 +50,11 @@ module ActiveRecord end private - def method_body; raise NotImplementedError; end + + # Override this method in the subclasses for method body. + def method_body(method_name, const_name) + raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." + end end module ClassMethods @@ -66,6 +72,7 @@ module ActiveRecord # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: + return false if @attribute_methods_generated # Use a mutex; we don't want two thread simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do @@ -187,6 +194,26 @@ module ActiveRecord [] end end + + # Returns the column object for the named attribute. + # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the + # named attribute does not exist. + # + # class Person < ActiveRecord::Base + # end + # + # person = Person.new + # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter + # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # + # person.column_for_attribute(:nothing) + # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> + def column_for_attribute(name) + name = name.to_s + columns_hash.fetch(name) do + ConnectionAdapters::NullColumn.new(name) + end + end end # If we haven't generated any methods yet, generate them, then @@ -200,6 +227,7 @@ module ActiveRecord # this is probably horribly slow, but should only happen at most once for a given AR class attribute_method.bind(self).call(*args, &block) else + return super unless respond_to_missing?(method, true) send(method, *args, &block) end else @@ -233,9 +261,9 @@ module ActiveRecord # If the result is true then check for the select case. # For queries selecting a subset of columns, return false for unselected columns. - # We check defined?(@attributes) not to issue warnings if called on objects that + # We check defined?(@raw_attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + if defined?(@raw_attributes) && @raw_attributes.any? && self.class.column_names.include?(name) return has_attribute?(name) end @@ -252,7 +280,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) + @raw_attributes.has_key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -264,7 +292,7 @@ module ActiveRecord # person.attribute_names # # => ["id", "created_at", "updated_at", "name", "age"] def attribute_names - @attributes.keys + @raw_attributes.keys end # Returns a hash of all the attributes with their names as keys and the values of the attributes as values. @@ -281,11 +309,6 @@ module ActiveRecord } end - # Placeholder so it can be overriden when needed by serialization - def attributes_for_coder # :nodoc: - attributes - end - # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 # characters, Date and Time attributes are returned in the @@ -338,27 +361,12 @@ module ActiveRecord !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end - # Returns the column object for the named attribute. Returns +nil+ if the - # named attribute not exists. - # - # class Person < ActiveRecord::Base - # end - # - # person = Person.new - # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> - # - # person.column_for_attribute(:nothing) - # # => nil - def column_for_attribute(name) - # FIXME: should this return a null object for columns that don't exist? - self.class.columns_hash[name.to_s] - end - # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example, # "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 @@ -416,7 +424,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@raw_attributes) && @raw_attributes.include?(attr_name) end private @@ -435,16 +443,16 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !readonly_attribute?(name) + attribute_names.reject do |name| + readonly_attribute?(name) end end # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.select do |name| - column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) + attribute_names.reject do |name| + pk_attribute?(name) && id.nil? end end @@ -453,7 +461,7 @@ module ActiveRecord end def pk_attribute?(name) - column_for_attribute(name).primary + name == self.class.primary_key end def typecasted_attribute_value(name) diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index f596a8b02e..4365f5a1a1 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,7 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - @attributes[attr_name.to_s] + @raw_attributes[attr_name.to_s] end # Returns a hash of attributes before typecasting and deserialization. @@ -57,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes + @raw_attributes end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 99070f127b..4e32b78e34 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -55,7 +55,7 @@ module ActiveRecord # optimistic locking) won't get written unless they get marked as changed self.class.columns.each do |c| attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr]) + changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @raw_attributes[attr]) end end @@ -94,33 +94,7 @@ module ActiveRecord end def _field_changed?(attr, old, value) - if column = column_for_attribute(attr) - if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || - changes_from_zero_to_string?(old, value)) - value = nil - else - value = column.type_cast(value) - end - end - - old != value - end - - def changes_from_nil_to_empty_string?(column, old, value) - # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values. - # Hence we don't record it as a change if the value changes from nil to ''. - # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll - # be typecast back to 0 (''.to_i => 0) - column.null && (old.nil? || old == 0) && value.blank? - end - - def changes_from_zero_to_string?(old, value) - # For columns with old 0 and value non-empty string - old == 0 && value.is_a?(String) && value.present? && non_zero?(value) - end - - def non_zero?(value) - value !~ /\A0+(\.0+)?\z/ + column_for_attribute(attr).changed?(old, value) end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index d01e9aea59..ae3785638a 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -22,7 +22,7 @@ module ActiveRecord # the attribute name. Using a constant means that we do not have # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to - # key the @attributes_cache in read_attribute. + # key the @attributes in read_attribute. def method_body(method_name, const_name) <<-EOMETHOD def #{method_name} @@ -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 @@ -94,7 +94,7 @@ module ActiveRecord def cacheable_column?(column) if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT - ! serialized_attributes.include? column.name + true else attribute_types_cached_by_default.include?(column.type) end @@ -108,22 +108,22 @@ module ActiveRecord # If it's cached, just return it # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829. name = attr_name.to_s - @attributes_cache[name] || @attributes_cache.fetch(name) { + @attributes[name] || @attributes.fetch(name) { column = @column_types_override[name] if @column_types_override column ||= @column_types[name] - return @attributes.fetch(name) { + return @raw_attributes.fetch(name) { if name == 'id' && self.class.primary_key != name read_attribute(self.class.primary_key) end } unless column - value = @attributes.fetch(name) { + value = @raw_attributes.fetch(name) { return block_given? ? yield(name) : nil } if self.class.cache_attribute?(name) - @attributes_cache[name] = column.type_cast(value) + @attributes[name] = column.type_cast(value) else column.type_cast value end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index c3466153d6..148fc9eae5 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -58,74 +58,24 @@ module ActiveRecord Coders::YAMLColumn.new(class_name_or_coder) end + type = columns_hash[attr_name.to_s].cast_type + if type.serialized? + type = type.subtype + end + property attr_name, Type::Serialized.new(type, coder) + # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy # has its own hash of own serialized attributes self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder) end end - class Type # :nodoc: - def initialize(column) - @column = column - end - - def type_cast(value) - if value.state == :serialized - value.unserialized_value @column.type_cast value.value - else - value.unserialized_value - end - end - - def type - @column.type - end - - def accessor - ActiveRecord::Store::IndifferentHashAccessor - end - end - - class Attribute < Struct.new(:coder, :value, :state) # :nodoc: - def unserialized_value(v = value) - state == :serialized ? unserialize(v) : value - end - - def serialized_value - state == :unserialized ? serialize : value - end - - def unserialize(v) - self.state = :unserialized - self.value = coder.load(v) - end - - def serialize - self.state = :serialized - self.value = coder.dump(value) - end - end # This is only added to the model when serialize is called, which # ensures we do not make things slower when serialization is not used. module Behavior # :nodoc: extend ActiveSupport::Concern - module ClassMethods # :nodoc: - def initialize_attributes(attributes, options = {}) - serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized - super(attributes, options) - - serialized_attributes.each do |key, coder| - if attributes.key?(key) - attributes[key] = Attribute.new(coder, attributes[key], serialized) - end - end - - attributes - end - end - def should_record_timestamps? super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) end @@ -133,58 +83,6 @@ module ActiveRecord def keys_for_partial_write super | (attributes.keys & self.class.serialized_attributes.keys) end - - def type_cast_attribute_for_write(column, value) - if column && coder = self.class.serialized_attributes[column.name] - Attribute.new(coder, value, :unserialized) - else - super - end - end - - def _field_changed?(attr, old, value) - if self.class.serialized_attributes.include?(attr) - old != value - else - super - end - end - - def read_attribute_before_type_cast(attr_name) - if self.class.serialized_attributes.include?(attr_name) - super.unserialized_value - else - super - end - end - - def attributes_before_type_cast - super.dup.tap do |attributes| - self.class.serialized_attributes.each_key do |key| - if attributes.key?(key) - attributes[key] = attributes[key].unserialized_value - end - end - end - end - - def typecasted_attribute_value(name) - if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - super - end - end - - def attributes_for_coder - attribute_names.each_with_object({}) do |name, attrs| - attrs[name] = if self.class.serialized_attributes.include?(name) - @attributes[name].serialized_value - else - read_attribute(name) - end - end - end end end end 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..18778698e8 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -2,6 +2,8 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -10,10 +12,6 @@ module ActiveRecord value = @column.type_cast(value) value.acts_like?(:time) ? value.in_time_zone : value end - - def type - @column.type - end end extend ActiveSupport::Concern @@ -28,7 +26,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]) @@ -38,7 +36,7 @@ module ActiveRecord previous_time = attribute_changed?("#{attr_name}") ? changed_attributes["#{attr_name}"] : read_attribute(:#{attr_name}) write_attribute(:#{attr_name}, time) #{attr_name}_will_change! if previous_time != time_with_zone - @attributes_cache["#{attr_name}"] = time_with_zone + @attributes["#{attr_name}"] = time_with_zone end EOV generated_attribute_methods.module_eval(method_body, __FILE__, line) @@ -51,7 +49,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/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c853fc0917..5203b30462 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -26,8 +26,6 @@ module ActiveRecord protected if Module.methods_transplantable? - # See define_method_attribute in read.rb for an explanation of - # this code. def define_method_attribute=(name) method = WriterMethodCache[name] generated_attribute_methods.module_eval { @@ -55,24 +53,12 @@ module ActiveRecord # specified +value+. Empty strings for fixnum and float columns are # turned into +nil+. def write_attribute(attr_name, value) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - @attributes_cache.delete(attr_name) - column = column_for_attribute(attr_name) - - # If we're dealing with a binary column, write the data to the cache - # so we don't attempt to typecast multiple times. - if column && column.binary? - @attributes_cache[attr_name] = value - end + write_attribute_with_type_cast(attr_name, value, true) + end - if column || @attributes.has_key?(attr_name) - @attributes[attr_name] = type_cast_attribute_for_write(column, value) - else - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end + def raw_write_attribute(attr_name, value) + write_attribute_with_type_cast(attr_name, value, false) end - alias_method :raw_write_attribute, :write_attribute private # Handle *= for method_missing. @@ -80,10 +66,27 @@ module ActiveRecord write_attribute(attribute_name, value) end - def type_cast_attribute_for_write(column, value) - return value unless column + def write_attribute_with_type_cast(attr_name, value, should_type_cast) + attr_name = attr_name.to_s + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key + @attributes.delete(attr_name) + column = column_for_attribute(attr_name) + + unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" + end - column.type_cast_for_write value + # If we're dealing with a binary column, write the data to the cache + # so we don't attempt to typecast multiple times. + if column.binary? + @attributes[attr_name] = value + end + + if should_type_cast + @raw_attributes[attr_name] = column.type_cast_for_write(value) + else + @raw_attributes[attr_name] = value + end end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 80cf7572df..74e2a8e6b9 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -147,6 +147,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) + return if method_defined?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -179,30 +180,28 @@ module ActiveRecord validation_method = :"validate_associated_records_for_#{reflection.name}" collection = reflection.collection? - unless method_defined?(save_method) - if collection - before_save :before_save_collection_association - - define_non_cyclic_method(save_method) { save_collection_association(reflection) } - # Doesn't use after_save as that would save associations added in after_create/after_update twice - after_create save_method - after_update save_method - elsif reflection.macro == :has_one - define_method(save_method) { save_has_one_association(reflection) } - # Configures two callbacks instead of a single after_save so that - # the model may rely on their execution order relative to its - # own callbacks. - # - # For example, given that after_creates run before after_saves, if - # we configured instead an after_save there would be no way to fire - # a custom after_create callback after the child association gets - # created. - after_create save_method - after_update save_method - else - define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } - before_save save_method - end + if collection + before_save :before_save_collection_association + + define_non_cyclic_method(save_method) { save_collection_association(reflection) } + # Doesn't use after_save as that would save associations added in after_create/after_update twice + after_create save_method + after_update save_method + elsif reflection.macro == :has_one + define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method) + # Configures two callbacks instead of a single after_save so that + # the model may rely on their execution order relative to its + # own callbacks. + # + # For example, given that after_creates run before after_saves, if + # we configured instead an after_save there would be no way to fire + # a custom after_create callback after the child association gets + # created. + after_create save_method + after_update save_method + else + define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) } + before_save save_method end if reflection.validate? && !method_defined?(validation_method) @@ -273,9 +272,11 @@ module ActiveRecord # go through nested autosave associations that are loaded in memory (without loading # any new ones), and return true if is changed for autosave def nested_records_changed_for_autosave? - self.class.reflect_on_all_autosave_associations.any? do |reflection| - association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + self.class._reflections.values.any? do |reflection| + if reflection.options[:autosave] + association = association_instance_get(reflection.name) + association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + end end end @@ -381,15 +382,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 db4d5f0129..8b0fffcf06 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -19,6 +19,7 @@ require 'active_record/errors' require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' +require 'active_record/properties' module ActiveRecord #:nodoc: # = Active Record @@ -321,6 +322,7 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store + include Properties end ActiveSupport.run_load_hooks(:active_record, Base) 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 bc47412405..7ff5001796 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -227,6 +227,10 @@ module ActiveRecord end end + def open_transactions + @transaction.number + end + def current_transaction #:nodoc: @transaction end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..04ae67234f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,34 +9,16 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return "'#{quote_string(value)}'" unless column - - case column.type - when :integer then value.to_i.to_s - when :float then value.to_f.to_s - else - "'#{quote_string(value)}'" - end - - when true, false - if column && column.type == :integer - value ? '1' : '0' - else - value ? quoted_true : quoted_false - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then "NULL" - when BigDecimal then value.to_s('F') - when Numeric, ActiveSupport::Duration then value.to_s - when Date, Time then "'#{quoted_date(value)}'" - when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" - else - "'#{quote_string(YAML.dump(value))}'" + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(value) end + + _quote(value) end # Cast a +value+ to a type that the database understands. For example, @@ -47,34 +29,19 @@ module ActiveRecord return value.id end - case value - when String, ActiveSupport::Multibyte::Chars - value = value.to_s - return value unless column - - case column.type - when :integer then value.to_i - when :float then value.to_f - else - value - end - - when true, false - if column && column.type == :integer - value ? 1 : 0 - else - value ? 't' : 'f' - end - # BigDecimals need to be put in a non-normalized form and quoted. - when nil then nil - when BigDecimal then value.to_s('F') - when Numeric then value - when Date, Time then quoted_date(value) - when Symbol then value.to_s - else - to_type = column ? " to #{column.type}" : "" - raise TypeError, "can't cast #{value.class}#{to_type}" + # FIXME: The only case we get an object other than nil or a real column + # is `SchemaStatements#add_column` with a PG array that has a non-empty default + # value. Is this really the only case? Are we missing tests for other types? + # We should have a real column object passed (or nil) here, and check for that + # instead + if column.respond_to?(:type_cast_for_database) + value = column.type_cast_for_database(value) end + + _type_cast(value) + rescue TypeError + to_type = column ? " to #{column.type}" : "" + raise TypeError, "can't cast #{value.class}#{to_type}" end # Quotes a string, escaping any ' (single quote) and \ (backslash) @@ -109,10 +76,18 @@ module ActiveRecord "'t'" end + def unquoted_true + 't' + end + def quoted_false "'f'" end + def unquoted_false + 'f' + end + def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal @@ -124,6 +99,45 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + end + + def _quote(value) + case value + when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + "'#{quote_string(value.to_s)}'" + when true then quoted_true + when false then quoted_false + when nil then "NULL" + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Numeric, ActiveSupport::Duration then value.to_s + when Date, Time then "'#{quoted_date(value)}'" + when Symbol then "'#{quote_string(value.to_s)}'" + when Class then "'#{value.to_s}'" + else + "'#{quote_string(YAML.dump(value))}'" + end + end + + def _type_cast(value) + case value + when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data + value.to_s + when true then unquoted_true + when false then unquoted_false + # BigDecimals need to be put in a non-normalized form and quoted. + when BigDecimal then value.to_s('F') + when Date, Time then quoted_date(value) + when *types_which_need_no_typecasting + value + else raise TypeError + end + end end end end 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..a9b3e9cfb9 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -99,9 +99,11 @@ 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 + # Note: The precision is the total number of significant digits + # and the scale is the number of digits that can be stored following # the decimal point. For example, the number 123.45 has a precision of 5 # and a scale of 2. A decimal with a precision of 5 and a scale of 2 can # range from -999.99 to 999.99. @@ -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_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index cdf0cbe218..d3e172927d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -20,19 +20,12 @@ module ActiveRecord def prepare_column_options(column, types) spec = {} spec[:name] = column.name.inspect - - # AR has an optimization which handles zero-scale decimals as integers. This - # code ensures that the dumper still dumps the column as a decimal. - spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type - 'decimal' - else - column.type.to_s - end - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal' + spec[:type] = column.type.to_s + spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale spec[:null] = 'false' unless column.null - spec[:default] = default_string(column.default) if column.has_default? + spec[:default] = column.type_cast_for_schema(column.default) if column.has_default? spec end @@ -40,31 +33,6 @@ module ActiveRecord def migration_keys [:name, :limit, :precision, :scale, :default, :null] end - - private - - def default_string(value) - case value - when BigDecimal - value.to_s - when Date, DateTime, Time - "'#{value.to_s(:db)}'" - when Range - # infinity dumps as Infinity, which causes uninitialized constant error - value.inspect.gsub('Infinity', '::Float::INFINITY') - when IPAddr - subnet_mask = value.instance_variable_get(:@mask_addr) - - # If the subnet mask is equal to /32, don't output it - if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" - else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" - end - else - value.inspect - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 3b3b03ff6e..cc494a7f40 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,6 +1,7 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' +require 'active_record/type' require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/abstract/schema_dumper' @@ -13,7 +14,10 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload :Column + autoload_at 'active_record/connection_adapters/column' do + autoload :Column + autoload :NullColumn + end autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do @@ -324,10 +328,6 @@ module ActiveRecord @connection end - def open_transactions - @transaction.number - end - def create_savepoint(name = nil) end @@ -360,8 +360,80 @@ module ActiveRecord pool.checkin self end + def type_map # :nodoc: + @type_map ||= Type::TypeMap.new.tap do |mapping| + initialize_type_map(mapping) + end + end + + def new_column(name, default, cast_type, sql_type = nil, null = true) + Column.new(name, default, cast_type, sql_type, null) + end + protected + def lookup_cast_type(sql_type) # :nodoc: + type_map.lookup(sql_type) + end + + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, %r(boolean)i, Type::Boolean + register_class_with_limit m, %r(char)i, Type::String + register_class_with_limit m, %r(binary)i, Type::Binary + register_class_with_limit m, %r(text)i, Type::Text + register_class_with_limit m, %r(date)i, Type::Date + register_class_with_limit m, %r(time)i, Type::Time + register_class_with_limit m, %r(datetime)i, Type::DateTime + register_class_with_limit m, %r(float)i, Type::Float + register_class_with_limit m, %r(int)i, Type::Integer + + m.alias_type %r(blob)i, 'binary' + m.alias_type %r(clob)i, 'text' + m.alias_type %r(timestamp)i, 'datetime' + m.alias_type %r(numeric)i, 'decimal' + m.alias_type %r(number)i, 'decimal' + m.alias_type %r(double)i, 'float' + + m.register_type(%r(decimal)i) do |sql_type| + scale = extract_scale(sql_type) + precision = extract_precision(sql_type) + + if scale == 0 + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) + else + Type::Decimal.new(precision: precision, scale: scale) + end + end + end + + def reload_type_map # :nodoc: + type_map.clear + initialize_type_map(type_map) + end + + def register_class_with_limit(mapping, key, klass) # :nodoc: + mapping.register_type(key) do |*args| + limit = extract_limit(args.last) + klass.new(limit: limit) + end + end + + def extract_scale(sql_type) # :nodoc: + case sql_type + when /\((\d+)\)/ then 0 + when /\((\d+)(,(\d+))\)/ then $3.to_i + end + end + + def extract_precision(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ + end + + def extract_limit(sql_type) # :nodoc: + $1.to_i if sql_type =~ /\((.*)\)/ + end + def translate_exception_class(e, sql) message = "#{e.class.name}: #{e.message}: #{sql}" @logger.error message if @logger 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 4184fad81c..759ac9943f 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) @@ -86,54 +86,12 @@ module ActiveRecord sql_type =~ /blob/i || type == :text end - # Must return the relevant concrete adapter - def adapter - raise NotImplementedError - end - def case_sensitive? collation && !collation.match(/_ci$/) end 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 - $1.split(',').map{|enum| enum.strip.length - 2}.max - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super - end - end - # MySQL misreports NOT NULL column default when none is given. # We can't detect this for columns which may have a legitimate '' # default (string) but we can for others (integer, datetime, boolean, @@ -173,7 +131,6 @@ module ActiveRecord :float => { :name => "float" }, :decimal => { :name => "decimal" }, :datetime => { :name => "datetime" }, - :timestamp => { :name => "datetime" }, :time => { :name => "time" }, :date => { :name => "date" }, :binary => { :name => "blob" }, @@ -221,17 +178,6 @@ module ActiveRecord true end - def type_cast(value, column) - case value - when TrueClass - 1 - when FalseClass - 0 - else - super - end - end - # MySQL 4 technically support transaction isolation, but it is affected by a bug # where the transaction level gets persisted for the whole session: # @@ -260,9 +206,8 @@ 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, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc: + 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 @@ -273,12 +218,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end @@ -296,10 +238,18 @@ module ActiveRecord QUOTED_TRUE end + def unquoted_true + 1 + end + def quoted_false QUOTED_FALSE end + def unquoted_false + 0 + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: @@ -315,6 +265,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) } @@ -468,7 +423,9 @@ module ActiveRecord execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) - new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra]) + sql_type = field[:Type] + cast_type = lookup_cast_type(sql_type) + new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra]) end end end @@ -642,6 +599,34 @@ module ActiveRecord protected + def initialize_type_map(m) # :nodoc: + super + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + Type::String.new(limit: limit) + end + + m.register_type %r(tinytext)i, Type::Text.new(limit: 255) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) + m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) + m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) + m.register_type %r(^int)i, Type::Integer.new(limit: 4) + m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) + m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) + m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + m.register_type %r(^float)i, Type::Float.new(limit: 24) + m.register_type %r(^double)i, Type::Float.new(limit: 53) + + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans + 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) diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 38efebeaf3..5e4e00bc64 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,109 +13,36 @@ 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_accessor :primary, :coder + attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function - alias :encoded? :coder + delegate :type, :precision, :scale, :limit, :klass, :accessor, + :text?, :number?, :binary?, :serialized?, :changed?, + :type_cast, :type_cast_for_write, :type_cast_for_database, + :type_cast_for_schema, + 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. - # Numeric columns will typecast boolean and string to appropriate numeric - # values. - 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 - end - end - # Returns the human name of the column name. # # ===== Examples @@ -127,174 +54,12 @@ module ActiveRecord def extract_default(default) type_cast(default) end + 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 + class NullColumn < Column + def initialize(name) + super name, nil, Type::Value.new end - - private - def extract_limit(sql_type) - $1.to_i if sql_type =~ /\((.*)\)/ - end - - 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/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 233af252d6..0a14cdfe89 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,13 +29,6 @@ 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) @@ -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..909bba8c7d 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 @@ -294,126 +262,70 @@ module ActiveRecord @connection.insert_id 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 + module Fields # :nodoc: + 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 = 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 @@ -447,7 +359,7 @@ module ActiveRecord end end - def execute_and_free(sql, name = nil) + def execute_and_free(sql, name = nil) # :nodoc: result = execute(sql, name) ret = yield result result.free @@ -460,7 +372,7 @@ module ActiveRecord end alias :create :insert_sql - def exec_delete(sql, name, binds) + def exec_delete(sql, name, binds) # :nodoc: affected_rows = 0 exec_query(sql, name, binds) do |n| 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..971f5eed7e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -1,41 +1,12 @@ 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) - 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) - case value - when /^0x/i - value[2..-1].hex.to_s(2) # Hexadecimal notation - else - value # Bit-string notation - 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 +16,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 +30,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 +38,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 +53,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,29 +67,7 @@ module ActiveRecord end end - def string_to_cidr(string) - if string.nil? - nil - elsif String === string - begin - IPAddr.new(string) - rescue ArgumentError - nil - end - else - string - end - end - - def cidr_to_string(object) - if IPAddr === object - "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}" - else - object - 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 1d22b56964..a579746815 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,161 +1,40 @@ +require 'active_record/connection_adapters/postgresql/cast' + module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, oid_type, sql_type = nil, null = true) - @oid_type = oid_type - default_value = self.class.extract_value_from_default(default) - + def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) if sql_type =~ /\[\]$/ @array = true - super(name, default_value, sql_type[0..sql_type.length - 3], null) + super(name, default, cast_type, sql_type[0..sql_type.length - 3], null) else @array = false - super(name, default_value, sql_type, null) + super(name, default, cast_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 + @default_function = default_function end # :stopdoc: class << self - include ConnectionAdapters::PostgreSQLColumn::Cast - include ConnectionAdapters::PostgreSQLColumn::ArrayParser - attr_accessor :money_precision - end - # :startdoc: + include PostgreSQL::Cast - # Extracts the value from a PostgreSQL column default definition. - def self.extract_value_from_default(default) - # This is a performance optimization for Ruby 1.9.2 in development. - # If the value is nil, we return nil straight away without checking - # the regular expressions. If we check each regular expression, - # Regexp#=== will call NilClass#to_str, which will trigger - # method_missing (defined by whiny nil in ActiveSupport) which - # makes this method very very slow. - return default unless default - - case default - when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m - $1 - # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ - $1 - # Character types - when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m - $1.gsub(/''/, "'") - # Binary data types - when /\A'(.*)'::bytea\z/m - $1 - # Date/time types - when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/ - $1 - when /\A'(.*)'::interval\z/ - $1 - # Boolean type - when 'true' - true - when 'false' - false - # Geometric types - when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/ - $1 - # Network address types - when /\A'(.*)'::(?:cidr|inet|macaddr)\z/ - $1 - # Bit string types - when /\AB'(.*)'::"?bit(?: varying)?"?\z/ - $1 - # XML type - when /\A'(.*)'::xml\z/m - $1 - # Arrays - when /\A'(.*)'::"?\D+"?\[\]\z/ - $1 - # Hstore - when /\A'(.*)'::hstore\z/ - $1 - # JSON - when /\A'(.*)'::json\z/ - $1 - # Object identifier types - when /\A-?\d+\z/ - $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. - nil + # 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 end - - # Casts a Ruby value to something appropriate for writing to PostgreSQL. - # see ActiveRecord::ConnectionAdapters::Class#type_cast_for_write - # see ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID::Type - 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 - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - end - - def extract_limit(sql_type) - case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - when /^timestamp/i; nil - else super - end - end - - # Extracts the scale from PostgreSQL-specific data types. - def extract_scale(sql_type) - # Money type has a fixed scale of 2. - sql_type =~ /^money/ ? 2 : super - end - - # Extracts the precision from PostgreSQL-specific data types. - def extract_precision(sql_type) - if sql_type == 'money' - self.class.money_precision - elsif sql_type =~ /timestamp/i - $1.to_i if sql_type =~ /\((\d+)\)/ - else - 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 + # :startdoc: 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 168b08ba75..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)}" @@ -94,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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 1e89f8cfd6..33a98b4fcb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,517 +1,33 @@ -require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/postgresql/oid/infinity' + +require 'active_record/connection_adapters/postgresql/oid/array' +require 'active_record/connection_adapters/postgresql/oid/bit' +require 'active_record/connection_adapters/postgresql/oid/bit_varying' +require 'active_record/connection_adapters/postgresql/oid/bytea' +require 'active_record/connection_adapters/postgresql/oid/cidr' +require 'active_record/connection_adapters/postgresql/oid/date' +require 'active_record/connection_adapters/postgresql/oid/date_time' +require 'active_record/connection_adapters/postgresql/oid/decimal' +require 'active_record/connection_adapters/postgresql/oid/enum' +require 'active_record/connection_adapters/postgresql/oid/float' +require 'active_record/connection_adapters/postgresql/oid/hstore' +require 'active_record/connection_adapters/postgresql/oid/inet' +require 'active_record/connection_adapters/postgresql/oid/integer' +require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/money' +require 'active_record/connection_adapters/postgresql/oid/point' +require 'active_record/connection_adapters/postgresql/oid/range' +require 'active_record/connection_adapters/postgresql/oid/specialized_string' +require 'active_record/connection_adapters/postgresql/oid/time' +require 'active_record/connection_adapters/postgresql/oid/uuid' +require 'active_record/connection_adapters/postgresql/oid/vector' + +require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter - module OID - class Type - def type; end - def simplified_type(sql_type); type end - - 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 - end - end - - class SpecializedString < OID::String - def type; @type end - - def initialize(type) - @type = type - end - end - - class Text < OID::String - def type; :text end - end - - class Bit < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_bit value - else - value - end - end - end - - class Bytea < Type - def type; :binary end - - def type_cast(value) - return if value.nil? - PGconn.unescape_bytea value - end - end - - class Money < Type - def type; :decimal end - - def type_cast(value) - return if value.nil? - return value unless ::String === value - - # Because money output is formatted according to the locale, there are two - # cases to consider (note the decimal separators): - # (1) $12,345,678.12 - # (2) $12.345.678,12 - # Negative values are represented as follows: - # (3) -$2.55 - # (4) ($2.55) - - value.sub!(/^\((.+)\)$/, '-\1') # (4) - case value - when /^-?\D+[\d,]+\.\d{2}$/ # (1) - value.gsub!(/[^-\d.]/, '') - when /^-?\D+[\d.]+,\d{2}$/ # (2) - value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') - end - - ConnectionAdapters::Column.value_to_decimal value - end - end - - class Vector < Type - attr_reader :delim, :subtype - - # +delim+ corresponds to the `typdelim` column in the pg_types - # table. +subtype+ is derived from the `typelem` column in the - # pg_types table. - def initialize(delim, subtype) - @delim = delim - @subtype = subtype - end - - # FIXME: this should probably split on +delim+ and use +subtype+ - # to cast the values. Unfortunately, the current Rails behavior - # is to just return the string. - def type_cast(value) - value - end - end - - class Point < Type - def type; :string end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value - else - value - end - end - end - - class Array < Type - def type; @subtype.type end - - attr_reader :subtype - def initialize(subtype) - @subtype = subtype - end - - def type_cast(value) - if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype - else - value - end - end - end - - class Range < Type - attr_reader :subtype - def simplified_type(sql_type); sql_type.to_sym end - - def initialize(subtype) - @subtype = subtype - end - - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast(value) - end - - def type_cast(value) - return if value.nil? || value == 'empty' - return value if value.is_a?(::Range) - - extracted = extract_bounds(value) - from = type_cast_single extracted[:from] - to = type_cast_single extracted[:to] - - if !infinity?(from) && extracted[:exclude_start] - if from.respond_to?(:succ) - from = from.succ - ActiveSupport::Deprecation.warn <<-MESSAGE -Excluding the beginning of a Range is only partialy supported through `#succ`. -This is not reliable and will be removed in the future. - MESSAGE - else - raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" - end - end - ::Range.new(from, to, extracted[:exclude_end]) - end - end - - class Integer < Type - def type; :integer end - - def type_cast(value) - return if value.nil? - - ConnectionAdapters::Column.value_to_integer value - end - end - - class Boolean < Type - def type; :boolean end - - 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) - :datetime - 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 - 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 - end - - class Float < Type - def type; :float end - - def type_cast(value) - case value - when nil; nil - when 'Infinity'; ::Float::INFINITY - when '-Infinity'; -::Float::INFINITY - when 'NaN'; ::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 - - def infinity(options = {}) - BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) - end - end - - class Enum < Type - def type; :enum end - - def type_cast(value) - value.to_s - end - end - - class Hstore < Type - 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? - - ConnectionAdapters::PostgreSQLColumn.string_to_hstore value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Cidr < Type - def type; :cidr end - def type_cast(value) - return if value.nil? - - ConnectionAdapters::PostgreSQLColumn.string_to_cidr value - end - end - class Inet < Cidr - def type; :inet end - end - - class Json < Type - 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? - - ConnectionAdapters::PostgreSQLColumn.string_to_json value - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - - class Uuid < Type - def type; :uuid end - def type_cast(value) - value.presence - end - end - - class TypeMap - def initialize - @mapping = {} - end - - def []=(oid, type) - @mapping[oid] = type - end - - def [](oid) - @mapping[oid] - end - - def clear - @mapping.clear - end - - def key?(oid) - @mapping.key? oid - 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 - end - - @mapping.fetch(ftype) { |oid| yield oid, fmod } - end - end - - # 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 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 - - private - def register_mapped_type(row) - register row['oid'], OID::NAMES[row['typname']] - end - - def register_enum_type(row) - register row['oid'], OID::Enum.new - end - - def register_array_type(row) - if subtype = @store[row['typelem'].to_i] - register row['oid'], OID::Array.new(subtype) - end - end - - def register_range_type(row) - if subtype = @store[row['rngsubtype'].to_i] - register row['oid'], OID::Range.new(subtype) - end - end - - def register_domain_type(row) - if base_type = @store[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[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) - - @store[oid] = oid_type - end - end - - # When the PG adapter connects, the pg_type table is queried. The - # key of this hash maps to the `typname` column from the table. - # 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 - } - - # Register an OID type named +name+ with a typecasting object in - # +type+. +name+ should correspond to the `typname` column in - # the `pg_type` table. - def self.register_type(name, type) - NAMES[name] = type - end - - # Alias the +old+ type to the +new+ type. - def self.alias_type(new, old) - NAMES[new] = NAMES[old] - end - - # Is +name+ a registered type? - def self.registered_type?(name) - NAMES.key? name - end - - register_type 'int2', OID::Integer.new - alias_type 'int4', 'int2' - alias_type 'int8', 'int2' - alias_type 'oid', 'int2' - 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 - alias_type 'char', 'varchar' - alias_type 'name', 'varchar' - alias_type 'bpchar', 'varchar' - register_type 'bool', OID::Boolean.new - register_type 'bit', OID::Bit.new - alias_type 'varbit', 'bit' - register_type 'timestamp', OID::Timestamp.new - alias_type 'timestamptz', 'timestamp' - register_type 'date', OID::Date.new - register_type 'time', OID::Time.new - - register_type 'money', OID::Money.new - register_type 'bytea', OID::Bytea.new - register_type 'point', OID::Point.new - register_type 'hstore', OID::Hstore.new - register_type 'json', OID::Json.new - register_type 'cidr', OID::Cidr.new - register_type 'inet', OID::Inet.new - register_type 'uuid', OID::Uuid.new - register_type 'xml', SpecializedString.new(:xml) - register_type 'tsvector', SpecializedString.new(:tsvector) - register_type 'macaddr', SpecializedString.new(:macaddr) - register_type 'citext', SpecializedString.new(:citext) - register_type 'ltree', SpecializedString.new(:ltree) - - # FIXME: why are we keeping these types as strings? - alias_type 'interval', 'varchar' - alias_type 'path', 'varchar' - alias_type 'line', 'varchar' - alias_type 'polygon', 'varchar' - alias_type 'circle', 'varchar' - alias_type 'lseg', 'varchar' - alias_type 'box', 'varchar' + module PostgreSQL + module OID # :nodoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb new file mode 100644 index 0000000000..0e9dcd8c0c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Array < Type::Value + attr_reader :subtype + delegate :type, to: :subtype + + def initialize(subtype) + @subtype = subtype + end + + def type_cast(value) + if ::String === value + ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb new file mode 100644 index 0000000000..3073f8ff30 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bit < Type::Value + def type + :bit + end + + def type_cast(value) + if ::String === value + case value + when /^0x/i + value[2..-1].hex.to_s(2) # Hexadecimal notation + else + value # Bit-string notation + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb new file mode 100644 index 0000000000..054af285bb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class BitVarying < OID::Bit + def type + :bit_varying + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb new file mode 100644 index 0000000000..36c53d8732 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Bytea < Type::Binary + def cast_value(value) + PGconn.unescape_bytea value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb new file mode 100644 index 0000000000..534961a414 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Cidr < Type::Value + def type + :cidr + end + + def type_cast_for_schema(value) + subnet_mask = value.instance_variable_get(:@mask_addr) + + # If the subnet mask is equal to /32, don't output it + if subnet_mask == (2**32 - 1) + "\"#{value.to_s}\"" + else + "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + end + end + + def type_cast_for_database(value) + if IPAddr === value + "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + else + value + end + end + + def cast_value(value) + if value.nil? + nil + elsif String === value + begin + IPAddr.new(value) + rescue ArgumentError + nil + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb new file mode 100644 index 0000000000..3c30ad5fec --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Date < Type::Date + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb new file mode 100644 index 0000000000..34e2276dd1 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class DateTime < Type::DateTime + include Infinity + + def cast_value(value) + if value.is_a?(::String) + case value + when 'infinity' then ::Float::INFINITY + when '-infinity' then -::Float::INFINITY + when / BC$/ + astronomical_year = format("%04d", -value[/^\d+/].to_i + 1) + super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year)) + else + super + end + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb new file mode 100644 index 0000000000..ed4b8911d9 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Decimal < Type::Decimal + def infinity(options = {}) + BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb new file mode 100644 index 0000000000..5fed8b0f89 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Enum < Type::Value + def type + :enum + end + + def type_cast(value) + value.to_s + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb new file mode 100644 index 0000000000..9753d71461 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Float < Type::Float + include Infinity + + def type_cast(value) + 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 + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb new file mode 100644 index 0000000000..bf680b6624 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Hstore < Type::Value + def type + :hstore + end + + def type_cast_for_write(value) + # roundtrip to ensure uniform uniform types + # TODO: This is not an efficient solution. + cast_value(type_cast_for_database(value)) + end + + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_hstore value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb new file mode 100644 index 0000000000..7ed8f5f031 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Inet < Cidr + def type + :inet + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb new file mode 100644 index 0000000000..d438ffa140 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + module Infinity + def infinity(options = {}) + options[:negative] ? -::Float::INFINITY : ::Float::INFINITY + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb new file mode 100644 index 0000000000..388d3dd9ed --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Integer < Type::Integer + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb new file mode 100644 index 0000000000..42a5110ffd --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Json < Type::Value + def type + :json + end + + def type_cast_for_write(value) + # roundtrip to ensure uniform uniform types + # TODO: This is not an efficient solution. + cast_value(type_cast_for_database(value)) + end + + def type_cast_for_database(value) + ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + end + + def cast_value(value) + ConnectionAdapters::PostgreSQLColumn.string_to_json value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb new file mode 100644 index 0000000000..d25eb256c2 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -0,0 +1,43 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Money < Type::Decimal + include Infinity + + class_attribute :precision + + def type + :money + end + + def scale + 2 + end + + def cast_value(value) + return value unless ::String === value + + # Because money output is formatted according to the locale, there are two + # cases to consider (note the decimal separators): + # (1) $12,345,678.12 + # (2) $12.345.678,12 + # Negative values are represented as follows: + # (3) -$2.55 + # (4) ($2.55) + + value.sub!(/^\((.+)\)$/, '-\1') # (4) + case value + when /^-?\D+[\d,]+\.\d{2}$/ # (1) + value.gsub!(/[^-\d.]/, '') + when /^-?\D+[\d.]+,\d{2}$/ # (2) + value.gsub!(/[^-\d,]/, '').sub!(/,/, '.') + end + + super(value) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb new file mode 100644 index 0000000000..9007bfb178 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -0,0 +1,24 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Point < Type::Value + def type + :point + end + + def type_cast(value) + if ::String === value + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + value.split(',').map{ |v| Float(v) } + else + value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb new file mode 100644 index 0000000000..a0d8a94c74 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -0,0 +1,60 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Range < Type::Value + attr_reader :subtype, :type + + def initialize(subtype, type) + @subtype = subtype + @type = type + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end + + def type_cast_for_schema(value) + value.inspect.gsub('Infinity', '::Float::INFINITY') + end + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast(value) + end + + def cast_value(value) + return if value == 'empty' + return value if value.is_a?(::Range) + + extracted = extract_bounds(value) + from = type_cast_single extracted[:from] + to = type_cast_single extracted[:to] + + if !infinity?(from) && extracted[:exclude_start] + if from.respond_to?(:succ) + from = from.succ + ActiveSupport::Deprecation.warn <<-MESSAGE +Excluding the beginning of a Range is only partialy supported through `#succ`. +This is not reliable and will be removed in the future. + MESSAGE + else + raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" + end + end + ::Range.new(from, to, extracted[:exclude_end]) + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb new file mode 100644 index 0000000000..7b1ca16bc4 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class SpecializedString < Type::String + attr_reader :type + + def initialize(type) + @type = type + end + + def text? + false + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb new file mode 100644 index 0000000000..ea1f599b0f --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -0,0 +1,11 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Time < Type::Time + include Infinity + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb new file mode 100644 index 0000000000..28f7a4eafb --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -0,0 +1,85 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + # 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 run(records) + nodes = records.reject { |row| @store.key? row['oid'].to_i } + mapped, nodes = nodes.partition { |row| @store.key? 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 + + private + def register_mapped_type(row) + alias_type row['oid'], row['typname'] + end + + def register_enum_type(row) + register row['oid'], OID::Enum.new + end + + def register_array_type(row) + if subtype = @store.lookup(row['typelem'].to_i) + register row['oid'], OID::Array.new(subtype) + end + end + + 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 = assert_valid_registration(oid, oid_type) + @store.register_type(oid, oid_type) + end + + def alias_type(oid, target) + oid = assert_valid_registration(oid, target) + @store.alias_type(oid, target) + end + + def assert_valid_registration(oid, oid_type) + raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? + oid.to_i + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb new file mode 100644 index 0000000000..0ed5491887 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Uuid < Type::Value + def type + :uuid + end + + def type_cast(value) + value.presence + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb new file mode 100644 index 0000000000..2f7d1be197 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -0,0 +1,26 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Vector < Type::Value + attr_reader :delim, :subtype + + # +delim+ corresponds to the `typdelim` column in the pg_types + # table. +subtype+ is derived from the `typelem` column in the + # pg_types table. + def initialize(delim, subtype) + @delim = delim + @subtype = subtype + end + + # FIXME: this should probably split on +delim+ and use +subtype+ + # to cast the values. Unfortunately, the current Rails behavior + # is to just return the string. + def type_cast(value) + value + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index fa458d0243..3cf40e6cd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL module Quoting # Escapes binary strings for bytea input to the database. def escape_bytea(value) @@ -44,11 +44,6 @@ module ActiveRecord when 'json' then super(PostgreSQLColumn.json_to_string(value), column) else super end - when IPAddr - case sql_type - when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column) - else super - end when Float if value.infinite? && column.type == :datetime "'#{value.to_s.downcase}'" @@ -66,7 +61,6 @@ module ActiveRecord end when String case sql_type - when 'bytea' then "'#{escape_bytea(value)}'" when 'xml' then "xml '#{quote_string(value)}'" when /^bit/ case value @@ -110,27 +104,12 @@ module ActiveRecord super(value, column) end end - when String - if 'bytea' == column.sql_type - # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc - # for more information - { value: value, format: 1 } - else - super(value, column) - end when Hash case column.sql_type when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) when 'json' then PostgreSQLColumn.json_to_string(value) else super(value, column) end - when IPAddr - if %w(inet cidr).include? column.sql_type - PostgreSQLColumn.cidr_to_string(value) - else - super(value, column) - end else super(value, column) end @@ -150,14 +129,7 @@ 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) - else - table_name, name_part = extract_pg_identifier_from_name(name_part) - "#{quote_column_name(schema)}.#{quote_column_name(table_name)}" - end + Utils.extract_schema_qualified_name(name.to_s).quoted end def quote_table_name_for_assignment(table, attr) @@ -177,8 +149,9 @@ module ActiveRecord result = "#{result}.#{sprintf("%06d", value.usec)}" end - if value.year < 0 - result = result.sub(/^-/, "") + " BC" + if value.year <= 0 + bce_year = format("%04d", -value.year + 1) + result = result.sub(/^-?\d+/, bce_year) + " BC" end result end @@ -191,6 +164,27 @@ module ActiveRecord quote(value, column) end end + + private + + def _quote(value) + if value.is_a?(Type::Binary::Data) + "'#{escape_bytea(value.to_s)}'" + else + super + end + end + + def _type_cast(value) + if value.is_a?(Type::Binary::Data) + # Return a bind param hash with format as binary. + # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # for more information + { value: value.to_s, format: 1 } + else + super + 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_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb new file mode 100644 index 0000000000..0867e5ef54 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,150 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module ColumnMethods + def xml(*args) + options = args.extract_options! + column(args[0], :xml, options) + end + + def tsvector(*args) + options = args.extract_options! + column(args[0], :tsvector, options) + end + + def int4range(name, options = {}) + column(name, :int4range, options) + end + + def int8range(name, options = {}) + column(name, :int8range, options) + end + + def tsrange(name, options = {}) + column(name, :tsrange, options) + end + + def tstzrange(name, options = {}) + column(name, :tstzrange, options) + end + + def numrange(name, options = {}) + column(name, :numrange, options) + end + + def daterange(name, options = {}) + column(name, :daterange, options) + end + + def hstore(name, options = {}) + column(name, :hstore, options) + end + + def ltree(name, options = {}) + column(name, :ltree, options) + end + + def inet(name, options = {}) + column(name, :inet, options) + end + + def cidr(name, options = {}) + column(name, :cidr, options) + end + + def macaddr(name, options = {}) + column(name, :macaddr, options) + end + + def uuid(name, options = {}) + column(name, :uuid, options) + end + + def json(name, options = {}) + column(name, :json, options) + end + + def citext(name, options = {}) + column(name, :citext, options) + end + + def point(name, options = {}) + column(name, :point, options) + end + + def bit(name, options) + column(name, :bit, options) + end + + def bit_varying(name, options) + column(name, :bit_varying, options) + end + + def money(name, options) + column(name, :money, options) + end + end + + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :array + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + # Defines the primary key field. + # Use of the native PostgreSQL UUID type is supported, and can be used + # by defining your tables as such: + # + # create_table :stuffs, id: :uuid do |t| + # t.string :content + # t.timestamps + # end + # + # By default, this will use the +uuid_generate_v4()+ function from the + # +uuid-ossp+ extension, which MUST be enabled on your database. To enable + # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your + # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can + # set the +:default+ option to +nil+: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: nil + # t.uuid :foo_id + # t.timestamps + # end + # + # You may also pass a different UUID generation function from +uuid-ossp+ + # or another library. + # + # Note that setting the UUID primary key default value to +nil+ will + # require you to assure that you always provide a UUID value before saving + # a record (as primary keys cannot be +nil+). This might be done via the + # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. + def primary_key(name, type = :primary_key, options = {}) + return super unless type == :uuid + options[:default] = options.fetch(:default, 'uuid_generate_v4()') + options[:primary_key] = true + column name, type, options + end + + def column(name, type = nil, options = {}) + super + column = self[name] + column.array = options[:array] + + self + end + + private + + def create_column_definition(name, type) + PostgreSQL::ColumnDefinition.new name, type + end + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + end + end + end +end 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 5bf4c7afd6..b2aeb3a058 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -1,11 +1,11 @@ module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter < AbstractAdapter + module PostgreSQL class SchemaCreation < AbstractAdapter::SchemaCreation private def visit_AddColumn(o) - sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale) + sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" 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+. @@ -101,16 +97,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) - schema, table = Utils.extract_schema_and_table(name.to_s) - return false unless table + name = Utils.extract_schema_qualified_name(name.to_s) + return false unless name.identifier exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = '#{table.gsub(/(^"|"$)/,'')}' - AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'} + AND c.relname = '#{name.identifier}' + AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end @@ -182,11 +178,17 @@ 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 = get_oid_type(oid.to_i, fmod.to_i, column_name) - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type) + default_value = extract_value_from_default(oid, default) + default_function = extract_default_function(default_value, default) + new_column(column_name, default_value, oid, type, notnull == 'f', default_function) end end + def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc: + PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function) + end + # Returns the current database name. def current_database query('select current_database()', 'SCHEMA')[0][0] @@ -271,9 +273,9 @@ module ActiveRecord def default_sequence_name(table_name, pk = nil) #:nodoc: result = serial_sequence(table_name, pk || 'id') return nil unless result - result.split('.').last + Utils.extract_schema_qualified_name(result) rescue ActiveRecord::StatementInvalid - "#{table_name}_#{pk || 'id'}_seq" + PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq") end def serial_sequence(table, column) @@ -310,17 +312,19 @@ module ActiveRecord # First try looking for a sequence with a dependency on the # given table's primary key. result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, seq.relname + SELECT attr.attname, nsp.nspname, seq.relname FROM pg_class seq, pg_attribute attr, pg_depend dep, - pg_constraint cons + pg_constraint cons, + pg_namespace nsp WHERE seq.oid = dep.objid AND seq.relkind = 'S' AND attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid AND attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + AND seq.relnamespace = nsp.oid AND cons.contype = 'p' AND dep.classid = 'pg_class'::regclass AND dep.refobjid = '#{quote_table_name(table)}'::regclass @@ -328,7 +332,7 @@ module ActiveRecord if result.nil? or result.empty? result = query(<<-end_sql, 'SCHEMA')[0] - SELECT attr.attname, + SELECT attr.attname, nsp.nspname, CASE WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN @@ -340,13 +344,19 @@ module ActiveRecord JOIN pg_attribute attr ON (t.oid = attrelid) JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) + JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid) WHERE t.oid = '#{quote_table_name(table)}'::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql end - [result.first, result.last] + pk = result.shift + if result.last + [pk, PostgreSQL::Name.new(*result)] + else + [pk, nil] + end rescue nil end @@ -374,7 +384,7 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq == "#{table_name}_#{pk}_seq" + if seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" end @@ -487,7 +497,7 @@ module ActiveRecord # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers - s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '') + s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } [super, *order_columns].join(', ') 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..0290bcb48c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -0,0 +1,66 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + # Value Object to hold a schema qualified name. + # This is usually the name of a PostgreSQL relation but it can also represent + # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent + # double quoting. + class Name # :nodoc: + SEPARATOR = "." + attr_reader :schema, :identifier + + def initialize(schema, identifier) + @schema, @identifier = unquote(schema), unquote(identifier) + end + + def to_s + parts.join SEPARATOR + end + + def quoted + parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + end + + def ==(o) + o.class == self.class && o.parts == parts + end + alias_method :eql?, :== + + def hash + parts.hash + end + + protected + def unquote(part) + return unless part + part.gsub(/(^"|"$)/,'') + end + + def parts + @parts ||= [@schema, @identifier].compact + end + end + + module Utils # :nodoc: + extend self + + # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> + # extracted from +string+. + # +schema+ is nil if not specified in +string+. + # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) + # +string+ 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_qualified_name(string) + table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + PostgreSQL::Name.new(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 4908c5a47f..283ca81f94 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,13 +1,15 @@ 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_definitions' 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 @@ -72,139 +74,6 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - - module ColumnMethods - def xml(*args) - options = args.extract_options! - column(args[0], 'xml', options) - end - - def tsvector(*args) - options = args.extract_options! - column(args[0], 'tsvector', options) - end - - def int4range(name, options = {}) - column(name, 'int4range', options) - end - - def int8range(name, options = {}) - column(name, 'int8range', options) - end - - def tsrange(name, options = {}) - column(name, 'tsrange', options) - end - - def tstzrange(name, options = {}) - column(name, 'tstzrange', options) - end - - def numrange(name, options = {}) - column(name, 'numrange', options) - end - - def daterange(name, options = {}) - column(name, 'daterange', options) - end - - def hstore(name, options = {}) - column(name, 'hstore', options) - end - - def ltree(name, options = {}) - column(name, 'ltree', options) - end - - def inet(name, options = {}) - column(name, 'inet', options) - end - - def cidr(name, options = {}) - column(name, 'cidr', options) - end - - def macaddr(name, options = {}) - column(name, 'macaddr', options) - end - - def uuid(name, options = {}) - column(name, 'uuid', options) - end - - def json(name, options = {}) - column(name, 'json', options) - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - end - - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition - include ColumnMethods - - # Defines the primary key field. - # Use of the native PostgreSQL UUID type is supported, and can be used - # by defining your tables as such: - # - # create_table :stuffs, id: :uuid do |t| - # t.string :content - # t.timestamps - # end - # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: - # - # create_table :stuffs, id: false do |t| - # t.primary_key :id, :uuid, default: nil - # t.uuid :foo_id - # t.timestamps - # end - # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. - # - # Note that setting the UUID primary key default value to +nil+ will - # require you to assure that you always provide a UUID value before saving - # a record (as primary keys cannot be +nil+). This might be done via the - # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. - def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options - end - - def citext(name, options = {}) - column(name, 'citext', options) - end - - def column(name, type = nil, options = {}) - super - column = self[name] - column.array = options[:array] - - self - end - - private - - def create_column_definition(name, type) - ColumnDefinition.new name, type - end - end - - class Table < ActiveRecord::ConnectionAdapters::Table - include ColumnMethods - end - ADAPTER_NAME = 'PostgreSQL' NATIVE_DATABASE_TYPES = { @@ -215,7 +84,6 @@ module ActiveRecord float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "timestamp" }, - timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, daterange: { name: "daterange" }, @@ -235,13 +103,19 @@ module ActiveRecord uuid: { name: "uuid" }, json: { name: "json" }, ltree: { name: "ltree" }, - citext: { name: "citext" } + citext: { name: "citext" }, + point: { name: "point" }, + bit: { name: "bit" }, + bit_varying: { name: "bit varying" }, + money: { name: "money" }, } - 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,9 +123,13 @@ module ActiveRecord ADAPTER_NAME end + def schema_creation # :nodoc: + PostgreSQL::SchemaCreation.new self + end + # Adds `:array` option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) + def prepare_column_options(column, types) # :nodoc: spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array spec[:default] = "\"#{column.default_function}\"" if column.default_function @@ -363,7 +241,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 @@ -494,25 +372,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 @@ -522,7 +381,7 @@ module ActiveRecord end def update_table_definition(table_name, base) #:nodoc: - Table.new(table_name, base) + PostgreSQL::Table.new(table_name, base) end protected @@ -551,27 +410,128 @@ module ActiveRecord private - def type_map - @type_map - end - - def get_oid_type(oid, fmod, column_name) + def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: if !type_map.key?(oid) - initialize_type_map(type_map, [oid]) + load_additional_types(type_map, [oid]) end - type_map.fetch(oid, fmod) { + type_map.fetch(oid, fmod, sql_type) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." - type_map[oid] = OID::Identity.new + Type::Value.new.tap do |cast_type| + type_map.register_type(oid, cast_type) + end } end - def reload_type_map - type_map.clear - initialize_type_map(type_map) + def initialize_type_map(m) # :nodoc: + register_class_with_limit m, 'int2', OID::Integer + m.alias_type 'int4', 'int2' + m.alias_type 'int8', 'int2' + m.alias_type 'oid', 'int2' + m.register_type 'float4', OID::Float.new + m.alias_type 'float8', 'float4' + m.register_type 'text', Type::Text.new + register_class_with_limit m, 'varchar', Type::String + m.alias_type 'char', 'varchar' + m.alias_type 'name', 'varchar' + m.alias_type 'bpchar', 'varchar' + m.register_type 'bool', Type::Boolean.new + register_class_with_limit m, 'bit', OID::Bit + register_class_with_limit m, 'varbit', OID::BitVarying + m.alias_type 'timestamptz', 'timestamp' + m.register_type 'date', OID::Date.new + m.register_type 'time', OID::Time.new + + m.register_type 'money', OID::Money.new + m.register_type 'bytea', OID::Bytea.new + m.register_type 'point', OID::Point.new + m.register_type 'hstore', OID::Hstore.new + m.register_type 'json', OID::Json.new + m.register_type 'cidr', OID::Cidr.new + m.register_type 'inet', OID::Inet.new + m.register_type 'uuid', OID::Uuid.new + m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) + m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) + m.register_type 'citext', OID::SpecializedString.new(:citext) + m.register_type 'ltree', OID::SpecializedString.new(:ltree) + + # FIXME: why are we keeping these types as strings? + m.alias_type 'interval', 'varchar' + m.alias_type 'path', 'varchar' + m.alias_type 'line', 'varchar' + m.alias_type 'polygon', 'varchar' + m.alias_type 'circle', 'varchar' + m.alias_type 'lseg', 'varchar' + m.alias_type 'box', 'varchar' + + m.register_type 'timestamp' do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::DateTime.new(precision: precision) + end + + m.register_type 'numeric' do |_, fmod, sql_type| + precision = extract_precision(sql_type) + scale = extract_scale(sql_type) + + # 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 fmod && (fmod - 4 & 0xffff).zero? + # FIXME: Remove this class, and the second argument to + # lookups on PG + Type::DecimalWithoutScale.new(precision: precision) + else + OID::Decimal.new(precision: precision, scale: scale) + end + end + + load_additional_types(m) + end + + def extract_limit(sql_type) # :nodoc: + case sql_type + when /^bigint/i; 8 + when /^smallint/i; 2 + else super + end end - def initialize_type_map(type_map, oids = nil) + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(oid, default) # :nodoc: + case default + # Quoted types + when /\A[\(B]?'(.*)'::/m + $1.gsub(/''/, "'") + # Boolean types + when 'true', 'false' + default + # Numeric types + when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + $1 + # Object identifier types + when /\A-?\d+\z/ + $1 + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. + nil + end + end + + def extract_default_function(default_value, default) # :nodoc: + default if has_default_function?(default_value, default) + end + + def has_default_function?(default_value, default) # :nodoc: + !default_value && (%r{\w+\(.*\)} === default) + end + + def load_additional_types(type_map, oids = nil) # :nodoc: if supports_ranges? query = <<-SQL SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype @@ -663,11 +623,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 @@ -676,7 +631,7 @@ module ActiveRecord # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision # should know about this but can't detect it there, so deal with it here. - PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10 + OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 configure_connection rescue ::PG::Error => error @@ -758,7 +713,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 @@ -770,23 +725,13 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) - match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/) - - if match_data - rest = name[match_data[0].length, name.length] - rest = rest[1, rest.length] if rest.start_with? "." - [match_data[1], (rest.length > 0 ? rest : nil)] - 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) - TableDefinition.new native_database_types, name, temporary, options, as + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as end end end diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index e5c9f6f54a..4d8afcf16a 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -12,11 +12,10 @@ module ActiveRecord @columns_hash = {} @primary_keys = {} @tables = {} - prepare_default_proc end def primary_keys(table_name) - @primary_keys[table_name] + @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end # A cached lookup for table existence. @@ -29,9 +28,9 @@ module ActiveRecord # Add internal cache for table with +table_name+. def add(table_name) if table_exists?(table_name) - @primary_keys[table_name] - @columns[table_name] - @columns_hash[table_name] + primary_keys(table_name) + columns(table_name) + columns_hash(table_name) end end @@ -40,14 +39,16 @@ module ActiveRecord end # Get the columns for a table - def columns(table) - @columns[table] + def columns(table_name) + @columns[table_name] ||= connection.columns(table_name) end # Get the columns for a table as a hash, key is the column name # value is the column object. - def columns_hash(table) - @columns_hash[table] + def columns_hash(table_name) + @columns_hash[table_name] ||= Hash[columns(table_name).map { |col| + [col.name, col] + }] end # Clears out internal caches @@ -76,32 +77,11 @@ module ActiveRecord def marshal_dump # if we get current version during initialization, it happens stack over flow. @version = ActiveRecord::Migrator.current_version - [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val| - Hash[val] - } + [@version, @columns, @columns_hash, @primary_keys, @tables] end def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array - prepare_default_proc - end - - private - - def prepare_default_proc - @columns.default_proc = Proc.new do |h, table_name| - h[table_name] = connection.columns(table_name) - end - - @columns_hash.default_proc = Proc.new do |h, table_name| - h[table_name] = Hash[columns(table_name).map { |col| - [col.name, col] - }] - end - - @primary_keys.default_proc = Proc.new do |h, table_name| - h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil - end end 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 737f2daa63..e6163771e8 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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" }, @@ -222,10 +219,9 @@ module ActiveRecord # QUOTING ================================================== - def quote(value, column = nil) - if value.kind_of?(String) && column && column.type == :binary - s = value.unpack("H*")[0] - "x'#{s}'" + def _quote(value) # :nodoc: + if value.is_a?(Type::Binary::Data) + "x'#{value.hex}'" else super end @@ -394,7 +390,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) + new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0) end end @@ -501,6 +499,12 @@ module ActiveRecord 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/core.rb b/activerecord/lib/active_record/core.rb index 4571cc0786..d6849fef2e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -249,10 +249,10 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = self.class.column_defaults.dup + defaults = self.class.raw_column_defaults.dup defaults.each { |k, v| defaults[k] = v.dup if v.duplicable? } - @attributes = self.class.initialize_attributes(defaults) + @raw_attributes = defaults @column_types_override = nil @column_types = self.class.column_types @@ -278,13 +278,15 @@ module ActiveRecord # post.init_with('attributes' => { 'title' => 'hello world' }) # post.title # => 'hello world' def init_with(coder) - @attributes = self.class.initialize_attributes(coder['attributes']) + @raw_attributes = coder['attributes'] @column_types_override = coder['column_types'] @column_types = self.class.column_types init_internals - @new_record = false + @new_record = coder['new_record'] + + self.class.define_attribute_methods run_callbacks :find run_callbacks :initialize @@ -321,16 +323,15 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast) - self.class.initialize_attributes(cloned_attributes, :serialized => false) - @attributes = cloned_attributes - @attributes[self.class.primary_key] = nil + @raw_attributes = cloned_attributes + @raw_attributes[self.class.primary_key] = nil run_callbacks(:initialize) unless _initialize_callbacks.empty? @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} + @attributes = {} @new_record = true @destroyed = false @@ -351,7 +352,8 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes_for_coder + coder['attributes'] = @raw_attributes + coder['new_record'] = new_record? end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -381,13 +383,13 @@ module ActiveRecord # accessible, even on destroyed records, but cloned models will not be # frozen. def freeze - @attributes = @attributes.clone.freeze + @raw_attributes = @raw_attributes.clone.freeze self end # Returns +true+ if the attributes hash has been frozen. def frozen? - @attributes.frozen? + @raw_attributes.frozen? end # Allows sort on objects @@ -416,9 +418,9 @@ module ActiveRecord # Returns the contents of the record as a nicely formatted string. def inspect - # We check defined?(@attributes) not to issue warnings if the object is + # We check defined?(@raw_attributes) not to issue warnings if the object is # allocated but not initialized. - inspection = if defined?(@attributes) && @attributes + inspection = if defined?(@raw_attributes) && @raw_attributes self.class.column_names.collect { |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" @@ -430,6 +432,29 @@ module ActiveRecord "#<#{self.class} #{inspection}>" end + # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record` + # when pp is required. + def pretty_print(pp) + pp.object_address_group(self) do + if defined?(@attributes) && @attributes + column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } + pp.seplist(column_names, proc { pp.text ',' }) do |column_name| + column_value = read_attribute(column_name) + pp.breakable ' ' + pp.group(1) do + pp.text column_name + pp.text ':' + pp.breakable + pp.pp column_value + end + end + else + pp.breakable ' ' + pp.text 'not initialized' + end + end + end + # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access @@ -494,11 +519,11 @@ module ActiveRecord def init_internals pk = self.class.primary_key - @attributes[pk] = nil unless @attributes.key?(pk) + @raw_attributes[pk] = nil unless @raw_attributes.key?(pk) @aggregation_cache = {} @association_cache = {} - @attributes_cache = {} + @attributes = {} @readonly = false @destroyed = false @marked_for_destruction = false diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index b7b790322a..05c4b13016 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 @@ -29,12 +34,11 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - belongs_to = child_class.reflect_on_all_associations(:belongs_to) - reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } 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 @@ -162,7 +166,7 @@ module ActiveRecord end def each_counter_cached_associations - reflections.each do |name, reflection| + _reflections.each do |name, reflection| yield association(name) if reflection.belongs_to? && reflection.counter_cache_column end end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 71efbb8f93..2ccb1b0702 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -217,6 +217,13 @@ module ActiveRecord class ImmutableRelation < ActiveRecordError end + # TransactionIsolationError will be raised under the following conditions: + # + # * The adapter does not support setting the isolation level + # * You are joining an existing open transaction + # * You are creating a nested (savepoint) transaction + # + # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 47d32fae05..f3d3cdc9e3 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -649,14 +649,14 @@ module ActiveRecord model_class end - reflection_class.reflect_on_all_associations.each do |association| + reflection_class._reflections.values.each do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s if association.name.to_s != fk_name && value = row.delete(association.name.to_s) - if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") + if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "") # support polymorphic belongs_to as "label (Type)" row[association.foreign_type] = $1 end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4d63b04d9f..7fb27ef6e9 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -66,7 +66,7 @@ module ActiveRecord send(lock_col + '=', previous_lock_value + 1) end - def _update_record(attribute_names = @attributes.keys) #:nodoc: + def _update_record(attribute_names = @raw_attributes.keys) #:nodoc: return super unless locking_enabled? return 0 if attribute_names.empty? diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index b6b02322d7..fca002873c 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 @@ -851,19 +851,6 @@ module ActiveRecord migrations(migrations_paths).last || NullMigration.new end - def proper_table_name(name, options = {}) - ActiveSupport::Deprecation.warn "ActiveRecord::Migrator.proper_table_name is deprecated and will be removed in Rails 4.2. Use the proper_table_name instance method on ActiveRecord::Migration instead" - options = { - table_name_prefix: ActiveRecord::Base.table_name_prefix, - table_name_suffix: ActiveRecord::Base.table_name_suffix - }.merge(options) - if name.respond_to? :table_name - name.table_name - else - "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}" - end - end - def migrations_paths @migrations_paths ||= ['db/migrate'] # just to not break things if someone uses: migration_path = some_string diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 002bd16976..baf2b5fbf8 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -198,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 @@ -217,20 +217,6 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - # Returns an array of column objects for the table associated with this class. - def columns - @columns ||= connection.schema_cache.columns(table_name).map do |col| - col = col.dup - col.primary = (col.name == primary_key) - col - end - end - - # Returns a hash of column objects for the table associated with this class. - def columns_hash - @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] - end - def column_types # :nodoc: @column_types ||= decorate_columns(columns_hash.dup) end @@ -238,14 +224,6 @@ module ActiveRecord def decorate_columns(columns_hash) # :nodoc: return if columns_hash.empty? - @serialized_column_names ||= self.columns_hash.keys.find_all do |name| - serialized_attributes.key?(name) - end - - @serialized_column_names.each do |name| - columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name]) - end - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| create_time_zone_conversion_attribute?(name, col) end.map!(&:first) @@ -263,6 +241,14 @@ module ActiveRecord @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] end + # Returns a hash where the keys are the column names and the values + # are the default values suitable for use in `@raw_attriubtes` + def raw_column_defaults # :nodoc: + @raw_column_defauts ||= Hash[column_defaults.map { |name, default| + [name, columns_hash[name].type_cast_for_write(default)] + }] + end + # Returns an array of column names as strings. def column_names @column_names ||= columns.map { |column| column.name } @@ -271,7 +257,7 @@ module ActiveRecord # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", # and columns used for single table inheritance have been removed. def content_columns - @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } + @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column } end # Resets all the cached information about columns, which will cause them @@ -307,26 +293,17 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil + @raw_column_defauts = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil @dynamic_methods_hash = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @relation = nil - @serialized_column_names = nil @time_zone_column_names = nil @cached_time_zone = nil end - # This is a hook for use by modules that need to do extra stuff to - # attributes when they are initialized. (e.g. attribute - # serialization) - def initialize_attributes(attributes, options = {}) #:nodoc: - attributes - end - private # Guesses the table name, but does not decorate it with prefix and suffix information. diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e6195e48a5..1242f49e28 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -305,7 +305,7 @@ module ActiveRecord options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank attr_names.each do |association_name| - if reflection = reflect_on_association(association_name) + if reflection = _reflect_on_association(association_name) reflection.autosave = true add_autosave_association_callbacks(reflection) @@ -516,7 +516,7 @@ module ActiveRecord # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) - ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) + Type::Boolean.new.type_cast(hash['_destroy']) end # Determines if a new record should be rejected by checking @@ -542,7 +542,7 @@ module ActiveRecord end def raise_nested_attributes_record_not_found!(association_name, record_id) - raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" + raise RecordNotFound, "Couldn't find #{self.class._reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}" end end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 05d0c41678..807c301596 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -23,7 +23,7 @@ module ActiveRecord end def size - 0 + calculate :size, nil end def empty? @@ -47,14 +47,28 @@ module ActiveRecord end def sum(*) - 0 + calculate :sum, nil + end + + def average(*) + calculate :average, nil + end + + def minimum(*) + calculate :minimum, nil + end + + def maximum(*) + calculate :maximum, nil end def calculate(operation, _column_name, _options = {}) # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. - if operation == :count + if [:count, :sum, :size].include? operation group_values.any? ? Hash.new : 0 + elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? + Hash.new else nil end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 13d7432773..2e3bcc0956 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,14 @@ 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, + 'new_record' => false, + ) end private @@ -179,8 +183,8 @@ module ActiveRecord # So any change to the attributes in either instance will affect the other. def becomes(klass) became = klass.new + became.instance_variable_set("@raw_attributes", @raw_attributes) became.instance_variable_set("@attributes", @attributes) - became.instance_variable_set("@attributes_cache", @attributes_cache) became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) @@ -396,11 +400,11 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) + @raw_attributes.update(fresh_object.instance_variable_get('@raw_attributes')) @column_types = self.class.column_types @column_types_override = fresh_object.instance_variable_get('@column_types_override') - @attributes_cache = {} + @attributes = {} self end @@ -490,7 +494,7 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. - def _update_record(attribute_names = @attributes.keys) + def _update_record(attribute_names = @raw_attributes.keys) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? 0 @@ -501,7 +505,7 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def _create_record(attribute_names = @attributes.keys) + def _create_record(attribute_names = @raw_attributes.keys) attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values diff --git a/activerecord/lib/active_record/properties.rb b/activerecord/lib/active_record/properties.rb new file mode 100644 index 0000000000..48ee42aaca --- /dev/null +++ b/activerecord/lib/active_record/properties.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module Properties # :nodoc: + extend ActiveSupport::Concern + + Type = ActiveRecord::Type + + included do + class_attribute :user_provided_columns, instance_accessor: false # :internal: + self.user_provided_columns = {} + end + + module ClassMethods + # Defines or overrides a property on this model. This allows customization of + # Active Record's type casting behavior, as well as adding support for user defined + # types. + # + # +name+ The name of the methods to define attribute methods for, and the column which + # this will persist to. + # + # +cast_type+ A type object that contains information about how to type cast the value. + # See the examples section for more information. + # + # ==== Options + # The options hash accepts the following options: + # + # +default+ is the default value that the column should use on a new record. + # + # ==== Examples + # + # The type detected by Active Record can be overriden. + # + # # db/schema.rb + # create_table :store_listings, force: true do |t| + # t.decimal :price_in_cents + # end + # + # # app/models/store_listing.rb + # class StoreListing < ActiveRecord::Base + # end + # + # store_listing = StoreListing.new(price_in_cents: '10.1') + # + # # before + # store_listing.price_in_cents # => BigDecimal.new(10.1) + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, Type::Integer.new + # end + # + # # after + # store_listing.price_in_cents # => 10 + # + # Users may also define their own custom types, as long as they respond to the methods + # defined on the value type. The `type_cast` method on your type object will be called + # with values both from the database, and from your controllers. See + # `ActiveRecord::Properties::Type::Value` for the expected API. It is recommended that your + # type objects inherit from an existing type, or the base value type. + # + # class MoneyType < ActiveRecord::Type::Integer + # def type_cast(value) + # if value.include?('$') + # price_in_dollars = value.gsub(/\$/, '').to_f + # price_in_dollars * 100 + # else + # value.to_i + # end + # end + # end + # + # class StoreListing < ActiveRecord::Base + # property :price_in_cents, MoneyType.new + # end + # + # store_listing = StoreListing.new(price_in_cents: '$10.00') + # store_listing.price_in_cents # => 1000 + def property(name, cast_type, options = {}) + name = name.to_s + clear_caches_calculated_from_columns + # Assign a new hash to ensure that subclasses do not share a hash + self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type)) + end + + # Returns an array of column objects for the table associated with this class. + def columns + @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name)) + end + + # Returns a hash of column objects for the table associated with this class. + def columns_hash + @columns_hash ||= Hash[columns.map { |c| [c.name, c] }] + end + + def reset_column_information # :nodoc: + super + clear_caches_calculated_from_columns + end + + private + + def add_user_provided_columns(schema_columns) + existing_columns = schema_columns.map do |column| + user_provided_columns[column.name] || column + end + + existing_column_names = existing_columns.map(&:name) + new_columns = user_provided_columns.except(*existing_column_names).values + + existing_columns + new_columns + end + + def clear_caches_calculated_from_columns + @columns = nil + @columns_hash = nil + @column_types = nil + @column_defaults = nil + @raw_column_defaults = nil + @column_names = nil + @content_columns = nil + end + end + end +end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index ef138c6f80..1fe54cea3f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -40,7 +40,7 @@ module ActiveRecord column_types = {} if result_set.respond_to? :column_types - column_types = result_set.column_types + column_types = result_set.column_types.merge(columns_hash) else ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 9e8e5fe94b..fa25ceaefa 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -366,7 +366,7 @@ namespace :railties do task :migrations => :'db:load_config' do to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } railties = {} - Rails.application.railties.each do |railtie| + Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0eec6774a0..4d5203612c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -6,9 +6,9 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :reflections + class_attribute :_reflections class_attribute :aggregate_reflections - self.reflections = {} + self._reflections = {} self.aggregate_reflections = {} end @@ -24,7 +24,7 @@ module ActiveRecord end def self.add_reflection(ar, name, reflection) - ar.reflections = ar.reflections.merge(name.to_s => reflection) + ar._reflections = ar._reflections.merge(name.to_s => reflection) end def self.add_aggregate_reflection(ar, name, reflection) @@ -53,6 +53,24 @@ module ActiveRecord aggregate_reflections[aggregation.to_s] end + # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. + # + # Account.reflections # => {balance: AggregateReflection} + # + # @api public + def reflections + ref = {} + _reflections.each do |name, reflection| + parent_name, parent_reflection = reflection.parent_reflection + if parent_name + ref[parent_name] = parent_reflection + else + ref[name] = reflection + end + end + ref + end + # Returns an array of AssociationReflection objects for all the # associations in the class. If you only want to reflect on a certain # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, @@ -63,6 +81,7 @@ module ActiveRecord # Account.reflect_on_all_associations # returns an array of all associations # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations # + # @api public def reflect_on_all_associations(macro = nil) association_reflections = reflections.values macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections @@ -73,11 +92,19 @@ module ActiveRecord # Account.reflect_on_association(:owner) # returns the owner AssociationReflection # Invoice.reflect_on_association(:line_items).macro # returns :has_many # + # @api public def reflect_on_association(association) reflections[association.to_s] end + # @api private + def _reflect_on_association(association) #:nodoc: + _reflections[association.to_s] + end + # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled. + # + # @api public def reflect_on_all_autosave_associations reflections.values.select { |reflection| reflection.options[:autosave] } end @@ -129,6 +156,10 @@ module ActiveRecord def autosave=(autosave) @automatic_inverse_of = false @options[:autosave] = autosave + _, parent_reflection = self.parent_reflection + if parent_reflection + parent_reflection.autosave = autosave + end end # Returns the class for the macro. @@ -193,10 +224,11 @@ module ActiveRecord end attr_reader :type, :foreign_type + attr_accessor :parent_reflection # [:name, Reflection] def initialize(macro, name, scope, options, active_record) super - @collection = :has_many == macro + @collection = [:has_many, :has_and_belongs_to_many].include?(macro) @automatic_inverse_of = nil @type = options[:as] && "#{options[:as]}_type" @foreign_type = options[:foreign_type] || "#{name}_type" @@ -207,7 +239,7 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements - if options[:polymorphic] + if polymorphic? key = [key, owner.read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @@ -271,7 +303,7 @@ module ActiveRecord end def check_validity_of_inverse! - unless options[:polymorphic] + unless polymorphic? if has_inverse? && inverse_of.nil? raise InverseOfAssociationNotFoundError.new(self) end @@ -330,12 +362,12 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def inverse_of return unless inverse_name - @inverse_of ||= klass.reflect_on_association inverse_name + @inverse_of ||= klass._reflect_on_association inverse_name end def polymorphic_inverse_of(associated_class) if has_inverse? - if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of]) + if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of]) inverse_relationship else raise InverseOfAssociationNotFoundError.new(self, associated_class) @@ -371,7 +403,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def association_class case macro when :belongs_to - if options[:polymorphic] + if polymorphic? Associations::BelongsToPolymorphicAssociation else Associations::BelongsToAssociation @@ -392,7 +424,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end def polymorphic? - options.key? :polymorphic + options[:polymorphic] end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] @@ -409,7 +441,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def calculate_constructable(macro, options) case macro when :belongs_to - !options[:polymorphic] + !polymorphic? when :has_one !options[:through] else @@ -436,7 +468,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi inverse_name = ActiveSupport::Inflector.underscore(active_record.name).to_sym begin - reflection = klass.reflect_on_association(inverse_name) + reflection = klass._reflect_on_association(inverse_name) rescue NameError # Give up: we couldn't compute the klass type so we won't be able # to find any associations either. @@ -535,7 +567,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection - through_reflection.klass.reflect_on_association(source_reflection_name) + through_reflection.klass._reflect_on_association(source_reflection_name) end # Returns the AssociationReflection object specified in the <tt>:through</tt> option @@ -551,7 +583,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection - active_record.reflect_on_association(options[:through]) + active_record._reflect_on_association(options[:through]) end # Returns an array of reflections which are involved in this association. Each item in the @@ -658,7 +690,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq names = names.find_all { |n| - through_reflection.klass.reflect_on_association(n) + through_reflection.klass._reflect_on_association(n) } if names.length > 1 @@ -691,7 +723,7 @@ directive on your declaration like: raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end - if through_reflection.options[:polymorphic] + if through_reflection.polymorphic? raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) end @@ -699,11 +731,11 @@ directive on your declaration like: raise HasManyThroughSourceAssociationNotFoundError.new(self) end - if options[:source_type] && source_reflection.options[:polymorphic].nil? + if options[:source_type] && !source_reflection.polymorphic? raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection) end - if source_reflection.options[:polymorphic] && options[:source_type].nil? + if source_reflection.polymorphic? && options[:source_type].nil? raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection) end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 0458018951..cef40be7ce 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -12,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 @@ -437,12 +438,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 diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 56cf9bcd27..11ab1b4595 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -177,7 +177,7 @@ module ActiveRecord end result = result.map do |attributes| - values = klass.initialize_attributes(attributes).values + values = attributes.values columns.zip(values).map { |column, value| column.type_cast value } end @@ -277,8 +277,8 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.to_sym) - associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations + association = @klass._reflect_on_association(group_attrs.first.to_sym) + associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else group_fields = group_attrs diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index db32ae12a8..47e90e9021 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -336,7 +336,16 @@ module ActiveRecord end def find_with_associations - join_dependency = construct_join_dependency + # NOTE: the JoinDependency constructed here needs to know about + # any joins already present in `self`, so pass them in + # + # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136 + # incorrect SQL is generated. In that case, the join dependency for + # SpecialCategorizations is constructed without knowledge of the + # preexisting join in joins_values to categorizations (by way of + # the `has_many :through` for categories). + # + join_dependency = construct_join_dependency(joins_values) aliases = join_dependency.aliases relation = select aliases.columns diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index fcb28a18f6..ac41d0aa80 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -156,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 d40f276968..eff5c8f09c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -26,7 +26,7 @@ module ActiveRecord queries << '1=0' else table = Arel::Table.new(column, default_table.engine) - association = klass.reflect_on_association(column.to_sym) + association = klass._reflect_on_association(column.to_sym) value.each do |k, v| queries.concat expand(association && association.klass, table, k, v) @@ -55,7 +55,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass.reflect_on_association(column.to_sym) + if klass && reflection = klass._reflect_on_association(column.to_sym) if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) queries << build(table[reflection.foreign_type], base_class) end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 2f6c34ac08..78dba8be06 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -2,28 +2,33 @@ module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: def call(attribute, value) + return attribute.in([]) if value.empty? + values = value.map { |x| x.is_a?(Base) ? x.id : x } ranges, values = values.partition { |v| v.is_a?(Range) } + nils, values = values.partition(&:nil?) - values_predicate = if values.include?(nil) - values = values.compact - + values_predicate = case values.length - when 0 - attribute.eq(nil) - when 1 - attribute.eq(values.first).or(attribute.eq(nil)) - else - attribute.in(values).or(attribute.eq(nil)) + when 0 then NullPredicate + when 1 then attribute.eq(values.first) + else attribute.in(values) end - else - attribute.in(values) + + unless nils.empty? + values_predicate = values_predicate.or(attribute.eq(nil)) end array_predicates = ranges.map { |range| attribute.in(range) } array_predicates << values_predicate array_predicates.inject { |composite, predicate| composite.or(predicate) } end + + module NullPredicate + def self.or(other) + other + end + end end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 416f2305d2..1262b2c291 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -573,15 +573,11 @@ WARNING end end - def where!(opts = :chain, *rest) # :nodoc: - if opts == :chain - WhereChain.new(self) - else - references!(PredicateBuilder.references(opts)) if Hash === opts + def where!(opts, *rest) # :nodoc: + references!(PredicateBuilder.references(opts)) if Hash === opts - self.where_values += build_where(opts, rest) - self - end + self.where_values += build_where(opts, rest) + self end # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition. @@ -950,7 +946,6 @@ WARNING [@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) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 228b2aa60f..293189fa69 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -95,7 +95,7 @@ module ActiveRecord @hash_rows ||= begin # We freeze the strings to prevent them getting duped when - # used as keys in ActiveRecord::Base's @attributes hash + # used as keys in ActiveRecord::Base's @raw_attributes hash columns = @columns.map { |c| c.dup.freeze } @rows.map { |row| # In the past we used Hash[columns.zip(row)] diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 1a766093d0..019fe2218e 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -180,12 +180,12 @@ module ActiveRecord #:nodoc: class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class - type = if klass.serialized_attributes.key?(name) + column = klass.columns_hash[name] || Type::Value.new + + type = if column.serialized? super - elsif klass.columns_hash.key?(name) - klass.columns_hash[name].type else - NilClass + column.type end { :text => :string, 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/transactions.rb b/activerecord/lib/active_record/transactions.rb index 17f76b63b3..d733063f5a 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 @@ -349,13 +347,18 @@ module ActiveRecord @_start_transaction_state[:destroyed] = @destroyed end @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = @attributes.frozen? + @_start_transaction_state[:frozen?] = @raw_attributes.frozen? end # 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. @@ -365,16 +368,16 @@ module ActiveRecord if transaction_level < 1 || force restore_state = @_start_transaction_state was_frozen = restore_state[:frozen?] - @attributes = @attributes.dup if @attributes.frozen? + @raw_attributes = @raw_attributes.dup if @raw_attributes.frozen? @new_record = restore_state[:new_record] @destroyed = restore_state[:destroyed] if restore_state.has_key?(:id) write_attribute(self.class.primary_key, restore_state[:id]) else + @raw_attributes.delete(self.class.primary_key) @attributes.delete(self.class.primary_key) - @attributes_cache.delete(self.class.primary_key) end - @attributes.freeze if was_frozen + @raw_attributes.freeze if was_frozen end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb new file mode 100644 index 0000000000..e9b827886a --- /dev/null +++ b/activerecord/lib/active_record/type.rb @@ -0,0 +1,19 @@ +require 'active_record/type/numeric' +require 'active_record/type/time_value' +require 'active_record/type/value' + +require 'active_record/type/binary' +require 'active_record/type/boolean' +require 'active_record/type/date' +require 'active_record/type/date_time' +require 'active_record/type/decimal' +require 'active_record/type/decimal_without_scale' +require 'active_record/type/float' +require 'active_record/type/integer' +require 'active_record/type/serialized' +require 'active_record/type/string' +require 'active_record/type/text' +require 'active_record/type/time' + +require 'active_record/type/type_map' +require 'active_record/type/hash_lookup_type_map' diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb new file mode 100644 index 0000000000..9d10c91fc1 --- /dev/null +++ b/activerecord/lib/active_record/type/binary.rb @@ -0,0 +1,35 @@ +module ActiveRecord + module Type + class Binary < Value # :nodoc: + def type + :binary + end + + def binary? + true + end + + def klass + ::String + end + + def type_cast_for_database(value) + Data.new(super) + end + + class Data + def initialize(value) + @value = value + end + + def to_s + @value + end + + def hex + @value.unpack('H*')[0] + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb new file mode 100644 index 0000000000..06dd17ed28 --- /dev/null +++ b/activerecord/lib/active_record/type/boolean.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class Boolean < Value # :nodoc: + def type + :boolean + end + + private + + def cast_value(value) + if value == '' + nil + else + ConnectionAdapters::Column::TRUE_VALUES.include?(value) + end + end + end + end +end diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb new file mode 100644 index 0000000000..d90a6069b7 --- /dev/null +++ b/activerecord/lib/active_record/type/date.rb @@ -0,0 +1,46 @@ +module ActiveRecord + module Type + class Date < Value # :nodoc: + def type + :date + end + + def klass + ::Date + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + 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 =~ ConnectionAdapters::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 diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb new file mode 100644 index 0000000000..560d63c101 --- /dev/null +++ b/activerecord/lib/active_record/type/date_time.rb @@ -0,0 +1,33 @@ +module ActiveRecord + 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 diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb new file mode 100644 index 0000000000..6eed005345 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal.rb @@ -0,0 +1,29 @@ +module ActiveRecord + module Type + class Decimal < Value # :nodoc: + include Numeric + + def type + :decimal + end + + def klass + ::BigDecimal + end + + def type_cast_for_schema(value) + value.to_s + 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 diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..cabdcecdd7 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,11 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb new file mode 100644 index 0000000000..dc50dae328 --- /dev/null +++ b/activerecord/lib/active_record/type/float.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module Type + class Float < Value # :nodoc: + include Numeric + + def type + :float + end + + def klass + ::Float + end + + alias type_cast_for_database type_cast + + private + + def cast_value(value) + value.to_f + end + end + end +end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb new file mode 100644 index 0000000000..bf92680268 --- /dev/null +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module Type + class HashLookupTypeMap < TypeMap # :nodoc: + delegate :key?, to: :@mapping + + def lookup(type, *args) + @mapping.fetch(type, proc { default_value }).call(type, *args) + end + + def fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) + end + + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } + end + end + end +end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb new file mode 100644 index 0000000000..1e2147dec9 --- /dev/null +++ b/activerecord/lib/active_record/type/integer.rb @@ -0,0 +1,27 @@ +module ActiveRecord + module Type + class Integer < Value # :nodoc: + include Numeric + + def type + :integer + end + + def klass + ::Fixnum + end + + alias type_cast_for_database type_cast + + 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 diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb new file mode 100644 index 0000000000..9cc6411e77 --- /dev/null +++ b/activerecord/lib/active_record/type/numeric.rb @@ -0,0 +1,41 @@ +module ActiveRecord + 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 + + def changed?(old_value, new_value) # :nodoc: + # 0 => 'wibble' should mark as changed so numericality validations run + if nil_or_zero?(old_value) && non_numeric_string?(new_value) + # nil => '' should not mark as changed + old_value != new_value.presence + else + super + end + end + + private + + def non_numeric_string?(value) + # 'wibble'.to_i will give zero, we want to make sure + # that we aren't marking int zero to string zero as + # changed. + value !~ /\A\d+\.?\d*\z/ + end + + def nil_or_zero?(value) + value.nil? || value == 0 + end + end + end +end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb new file mode 100644 index 0000000000..78a6d31e26 --- /dev/null +++ b/activerecord/lib/active_record/type/serialized.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module Type + class Serialized < SimpleDelegator # :nodoc: + attr_reader :subtype, :coder + + def initialize(subtype, coder) + @subtype = subtype + @coder = coder + super(subtype) + end + + def type_cast(value) + if is_default_value?(value) + value + else + coder.load(super) + end + end + + def type_cast_for_write(value) + return if value.nil? + unless is_default_value?(value) + coder.dump(value) + end + end + + alias type_cast_for_database type_cast_for_write + + def serialized? + true + end + + def accessor + ActiveRecord::Store::IndifferentHashAccessor + end + + private + + def changed?(old_value, new_value) # :nodoc: + old_value != new_value + end + + def is_default_value?(value) + value == coder.load(nil) + end + end + end +end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb new file mode 100644 index 0000000000..b3f6ca7691 --- /dev/null +++ b/activerecord/lib/active_record/type/string.rb @@ -0,0 +1,27 @@ +module ActiveRecord + 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 diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..26f980f060 --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,11 @@ +require 'active_record/type/string' + +module ActiveRecord + module Type + class Text < String # :nodoc: + def type + :text + end + end + end +end diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb new file mode 100644 index 0000000000..41f7d97f0c --- /dev/null +++ b/activerecord/lib/active_record/type/time.rb @@ -0,0 +1,26 @@ +module ActiveRecord + 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 diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb new file mode 100644 index 0000000000..d611d72dd4 --- /dev/null +++ b/activerecord/lib/active_record/type/time_value.rb @@ -0,0 +1,38 @@ +module ActiveRecord + module Type + module TimeValue # :nodoc: + def klass + ::Time + end + + def type_cast_for_schema(value) + "'#{value.to_s(:db)}'" + 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 =~ ConnectionAdapters::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 diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb new file mode 100644 index 0000000000..88c5f9c497 --- /dev/null +++ b/activerecord/lib/active_record/type/type_map.rb @@ -0,0 +1,48 @@ +module ActiveRecord + module Type + class TypeMap # :nodoc: + def initialize + @mapping = {} + end + + def lookup(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + 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, *args| + metadata = sql_type[/\(.*\)/, 0] + lookup("#{target_key}#{metadata}", *args) + end + end + + def clear + @mapping.clear + end + + private + + def default_value + @default_value ||= Value.new + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb new file mode 100644 index 0000000000..c072c1e2b6 --- /dev/null +++ b/activerecord/lib/active_record/type/value.rb @@ -0,0 +1,76 @@ +module ActiveRecord + module Type + class Value # :nodoc: + attr_reader :precision, :scale, :limit + + # Valid options are +precision+, +scale+, and +limit+. + # They are only used when dumping schema. + def initialize(options = {}) + options.assert_valid_keys(:precision, :scale, :limit) + @precision = options[:precision] + @scale = options[:scale] + @limit = options[:limit] + end + + # The simplified type that this object represents. Subclasses + # must override this method. + def type; end + + # Takes an input from the database, or from attribute setters, + # and casts it to a type appropriate for this object. This method + # should not be overriden by subclasses. Instead, override `cast_value`. + def type_cast(value) + cast_value(value) unless value.nil? + end + + def type_cast_for_database(value) + type_cast_for_write(value) + end + + def type_cast_for_schema(value) + value.inspect + end + + def text? + false + end + + def number? + false + end + + def binary? + false + end + + def serialized? + false + end + + def klass # :nodoc: + ::Object + end + + def type_cast_for_write(value) # :nodoc: + value + end + + # +old_value+ will always be type-cast. + # +new_value+ will come straight from the database + # or from assignment, so it could be anything. Types + # which cannot typecast arbitrary values should override + # this method. + def changed?(old_value, new_value) # :nodoc: + old_value != type_cast(new_value) + end + + private + + # Responsible for casting values from external sources to the appropriate + # type. Called by `type_cast` for all values except `nil`. + def cast_value(value) # :api: public + value + end + end + end +end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 9a19483da3..e586744818 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -4,7 +4,7 @@ module ActiveRecord def validate(record) super attributes.each do |attribute| - next unless record.class.reflect_on_association(attribute) + next unless record.class._reflect_on_association(attribute) associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index ee080451a9..2e7b1d7206 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -14,7 +14,6 @@ module ActiveRecord finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) - value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? @@ -47,7 +46,7 @@ module ActiveRecord end def build_relation(klass, table, attribute, value) #:nodoc: - if reflection = klass.reflect_on_association(attribute) + if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key value = value.attributes[reflection.primary_key_column.name] unless value.nil? end @@ -74,7 +73,7 @@ module ActiveRecord def scope_relation(record, table, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class.reflect_on_association(scope_item) + if reflection = record.class._reflect_on_association(scope_item) scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else @@ -86,12 +85,6 @@ module ActiveRecord relation end - def deserialize_attribute(record, attribute, value) - coder = record.class.serialized_attributes[attribute.to_s] - value = coder.dump value if value && coder - value - end - def map_enum_attribute(klass, attribute, value) mapping = klass.defined_enums[attribute.to_s] value = mapping[value] if value && mapping |