diff options
Diffstat (limited to 'activerecord')
78 files changed, 1023 insertions, 727 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 3dd973d64b..736745c3cd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,31 @@ +* Fix redefine a has_and_belongs_to_many inside inherited class + Fixing regression case, where redefining the same has_an_belongs_to_many + definition into a subclass would raise. + + Fixes #14983. + + *arthurnn* + +* Add a properties API to allow custom types and type casting behavior + to be specified. Will enable many edge cases to be deprecated, and + allow for additional interesting features in the future. + + *Sean Griffin* + +* Fix has_and_belongs_to_many public reflection. + When defining a has_and_belongs_to_many, internally we convert that to two has_many. + But as `reflections` is a public API, people expect to see the right macro. + + Fixes #14682. + + *arthurnn* + +* Fixed serialization for records with an attribute named `format`. + + Fixes #15188. + + *Godfrey Chan* + * When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` on a NullRelation should return a Hash. @@ -16,13 +44,14 @@ * Change belongs_to touch to be consistent with timestamp updates - If a model is set up with a belongs_to: touch relatinoship the parent + If a model is set up with a belongs_to: touch relationship the parent record will only be touched if the record was modified. This makes it consistent with timestamp updating on the record itself. *Brock Trappitt* -* Fixed the inferred table name of a HABTM auxiliar table inside a schema. +* Fixed the inferred table name of a has_and_belongs_to_many auxiliar + table inside a schema. Fixes #14824 @@ -65,7 +94,7 @@ *Aaron Nelson* -* Fix how to calculate associated class name when using namespaced `has_and_belongs_to_many` +* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many association. Fixes #14709. @@ -118,7 +147,7 @@ *Innokenty Mikhailov* -* Allow the PostgreSQL adapter to handle bigserial pk types again. +* Allow the PostgreSQL adapter to handle bigserial primary key types again. Fixes #10410. @@ -133,10 +162,10 @@ *Yves Senn* -* Fixed HABTM's CollectionAssociation size calculation. +* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. - HABTM should fall back to using the normal CollectionAssociation's size - calculation if the collection is not cached or loaded. + has_and_belongs_to_many should fall back to using the normal CollectionAssociation's + size calculation if the collection is not cached or loaded. Fixes #14913, #14914. @@ -180,10 +209,10 @@ *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* -* Stringify all variable keys of mysql connection configuration. +* Stringify all variables keys of MySQL connection configuration. - When the `sql_mode` variable for mysql adapters is set in the configuration - as a `String`, it was ignored and overwritten by the strict mode option. + When `sql_mode` variable for MySQL adapters set in configuration as `String` + was ignored and overwritten by strict mode option. Fixes #14895. 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/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 30b11c01eb..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 @@ -66,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/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index f5e911c739..2727e23870 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -100,7 +100,8 @@ 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 diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 5842be3a7b..01173b68f3 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 diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 42571d6af0..7519fec10a 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -143,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/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 6c2403d87e..a0a0214eae 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -48,7 +48,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 +70,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 @@ -456,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/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 53a9c874bf..47c6f94ba7 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -65,6 +65,8 @@ module ActiveRecord end class Type # :nodoc: + delegate :type, :type_cast_for_database, to: :@column + def initialize(column) @column = column end @@ -77,10 +79,6 @@ module ActiveRecord end end - def type - @column.type - end - def accessor ActiveRecord::Store::IndifferentHashAccessor 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 dfebb2cf56..6149ac4906 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 diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 1a4d2957ec..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 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/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 75501852ed..f836e60988 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,26 +9,22 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) + # 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 + 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. + "'#{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)}'" @@ -47,30 +43,25 @@ 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 + # 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 - 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 + case value + when Symbol, ActiveSupport::Multibyte::Chars + 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 Numeric then value when Date, Time then quoted_date(value) - when Symbol then value.to_s + when *types_which_need_no_typecasting + value else to_type = column ? " to #{column.type}" : "" raise TypeError, "can't cast #{value.class}#{to_type}" @@ -109,10 +100,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 +123,12 @@ module ActiveRecord value.to_s(:db) end + + private + + def types_which_need_no_typecasting + [nil, Numeric, String] + end end end end 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..ac14740cfe 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -20,15 +20,8 @@ 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 diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ea7703c82c..6ecd4efdc8 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -396,7 +396,8 @@ module ActiveRecord precision = extract_precision(sql_type) if scale == 0 - Type::Integer.new(precision: precision) + # FIXME: Remove this class as well + Type::DecimalWithoutScale.new(precision: precision) else Type::Decimal.new(precision: precision, scale: scale) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index d3f8470c30..82e62786ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -178,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: # @@ -234,8 +223,6 @@ module ActiveRecord 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") else super end @@ -253,10 +240,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: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 704868c058..86232f9d3f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -14,11 +14,12 @@ module ActiveRecord end attr_reader :name, :default, :cast_type, :null, :sql_type, :default_function - attr_accessor :primary, :coder + attr_accessor :coder alias :encoded? :coder - delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, :type_cast_for_write, to: :cast_type + delegate :type, :precision, :scale, :limit, :klass, :text?, :number?, :binary?, + :type_cast_for_write, :type_cast_for_database, to: :cast_type # Instantiates a new column in the table. # @@ -36,7 +37,6 @@ module ActiveRecord @null = null @default = extract_default(default) @default_function = nil - @primary = nil @coder = nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb index 0cbedb0987..f7bad20f00 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb @@ -6,13 +6,6 @@ module ActiveRecord "(#{point[0]},#{point[1]})" end - def string_to_point(string) # :nodoc: - if string[0] == '(' && string[-1] == ')' - string = string[1...-1] - end - string.split(',').map{ |v| Float(v) } - end - def string_to_bit(value) # :nodoc: case value when /^0x/i diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index b55766bde0..9a5e2d05ef 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -6,18 +6,16 @@ module ActiveRecord class PostgreSQLColumn < Column #:nodoc: attr_accessor :array - def initialize(name, default, cast_type, sql_type = nil, null = true) - 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, cast_type, 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, cast_type, sql_type, null) + super(name, default, cast_type, sql_type, null) end - @default_function = default if has_default_function?(default_value, default) + @default_function = default_function end # :stopdoc: @@ -38,78 +36,9 @@ module ActiveRecord end # :startdoc: - # 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 - end - end - def accessor cast_type.accessor end - - private - - def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)} === default) - 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 index 2769a8d3b4..f9531ddee3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -5,7 +5,10 @@ module ActiveRecord class Point < Type::String def type_cast(value) if ::String === value - ConnectionAdapters::PostgreSQLColumn.string_to_point value + if value[0] == '(' && value[-1] == ')' + value = value[1...-1] + end + value.split(',').map{ |v| Float(v) } else value end 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..bcfd605165 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -0,0 +1,134 @@ +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 + 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 c04a1d7178..484c44dc8d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -5,7 +5,7 @@ module ActiveRecord 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 @@ -179,7 +179,9 @@ module ActiveRecord # 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, type) - PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f') + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + PostgreSQLColumn.new(column_name, default_value, oid, type, notnull == 'f', default_function) 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 e3a2422160..027169ae3c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -6,6 +6,7 @@ require 'active_record/connection_adapters/postgresql/column' require 'active_record/connection_adapters/postgresql/oid' 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' @@ -73,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 = { @@ -251,13 +119,13 @@ module ActiveRecord ADAPTER_NAME end - def schema_creation + 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 @@ -509,7 +377,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 @@ -538,12 +406,12 @@ module ActiveRecord private - def get_oid_type(oid, fmod, column_name, sql_type = '') + def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc: if !type_map.key?(oid) load_additional_types(type_map, [oid]) end - type_map.fetch(normalize_oid_type(oid, fmod), sql_type) { + 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::Value.new.tap do |cast_type| type_map.register_type(oid, cast_type) @@ -551,24 +419,7 @@ module ActiveRecord } end - OID_FOR_DECIMAL_TREATED_AS_INT = 23 # :nodoc: - - def normalize_oid_type(ftype, fmod) - # The type for the numeric depends on the width of the field, - # so we'll do something special here. - # - # When dealing with decimal columns: - # - # places after decimal = fmod - 4 & 0xffff - # places before decimal = (fmod - 4) >> 16 & 0xffff - if ftype == 1700 && (fmod - 4 & 0xffff).zero? - OID_FOR_DECIMAL_TREATED_AS_INT - else - ftype - end - end - - def initialize_type_map(m) + def initialize_type_map(m) # :nodoc: register_class_with_limit m, 'int2', OID::Integer m.alias_type 'int4', 'int2' m.alias_type 'int8', 'int2' @@ -610,20 +461,29 @@ module ActiveRecord m.alias_type 'lseg', 'varchar' m.alias_type 'box', 'varchar' - m.register_type 'timestamp' do |_, sql_type| + m.register_type 'timestamp' do |_, _, sql_type| precision = extract_precision(sql_type) OID::DateTime.new(precision: precision) end - m.register_type 'numeric' do |_, sql_type| + m.register_type 'numeric' do |_, fmod, sql_type| precision = extract_precision(sql_type) scale = extract_scale(sql_type) - OID::Decimal.new(precision: precision, scale: scale) - end - m.register_type OID_FOR_DECIMAL_TREATED_AS_INT do |_, sql_type| - precision = extract_precision(sql_type) - OID::Integer.new(precision: precision) + # 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) @@ -637,7 +497,78 @@ module ActiveRecord end end - def load_additional_types(type_map, oids = nil) + # Extracts the value from a PostgreSQL column default definition. + def extract_value_from_default(default) # :nodoc: + # 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 + 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 @@ -831,23 +762,13 @@ module ActiveRecord end_sql end - def extract_pg_identifier_from_name(name) # :nodoc: - 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) # :nodoc: sql[/into\s+([^\(]*).*values\s*\(/im] $1.strip if $1 end def create_table_definition(name, temporary, options, as = nil) # :nodoc: - TableDefinition.new native_database_types, name, temporary, options, as + 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/type.rb b/activerecord/lib/active_record/connection_adapters/type.rb index 395a4160a8..bab7a3ff7e 100644 --- a/activerecord/lib/active_record/connection_adapters/type.rb +++ b/activerecord/lib/active_record/connection_adapters/type.rb @@ -7,6 +7,7 @@ require 'active_record/connection_adapters/type/boolean' require 'active_record/connection_adapters/type/date' require 'active_record/connection_adapters/type/date_time' require 'active_record/connection_adapters/type/decimal' +require 'active_record/connection_adapters/type/decimal_without_scale' require 'active_record/connection_adapters/type/float' require 'active_record/connection_adapters/type/integer' require 'active_record/connection_adapters/type/string' diff --git a/activerecord/lib/active_record/connection_adapters/type/binary.rb b/activerecord/lib/active_record/connection_adapters/type/binary.rb index 4b2d1a66e0..60afe44de1 100644 --- a/activerecord/lib/active_record/connection_adapters/type/binary.rb +++ b/activerecord/lib/active_record/connection_adapters/type/binary.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Binary < Value # :nodoc: + class Binary < Value def type :binary end diff --git a/activerecord/lib/active_record/connection_adapters/type/boolean.rb b/activerecord/lib/active_record/connection_adapters/type/boolean.rb index 2337bdd563..0d97379189 100644 --- a/activerecord/lib/active_record/connection_adapters/type/boolean.rb +++ b/activerecord/lib/active_record/connection_adapters/type/boolean.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Boolean < Value # :nodoc: + class Boolean < Value def type :boolean end diff --git a/activerecord/lib/active_record/connection_adapters/type/date.rb b/activerecord/lib/active_record/connection_adapters/type/date.rb index 1e7205fd0b..e8becbe1f4 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Date < Value # :nodoc: + class Date < Value def type :date end diff --git a/activerecord/lib/active_record/connection_adapters/type/date_time.rb b/activerecord/lib/active_record/connection_adapters/type/date_time.rb index c34f4c5a53..64f5d05301 100644 --- a/activerecord/lib/active_record/connection_adapters/type/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/date_time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class DateTime < Value # :nodoc: + class DateTime < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal.rb b/activerecord/lib/active_record/connection_adapters/type/decimal.rb index ac5af4b963..e93906ba19 100644 --- a/activerecord/lib/active_record/connection_adapters/type/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/type/decimal.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Decimal < Value # :nodoc: + class Decimal < Value include Numeric def type diff --git a/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb b/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb new file mode 100644 index 0000000000..e58c6e198d --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/type/decimal_without_scale.rb @@ -0,0 +1,13 @@ +require 'active_record/connection_adapters/type/integer' + +module ActiveRecord + module ConnectionAdapters + module Type + class DecimalWithoutScale < Integer # :nodoc: + def type + :decimal + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/type/float.rb b/activerecord/lib/active_record/connection_adapters/type/float.rb index 51cfa5d86a..f2427d2dfa 100644 --- a/activerecord/lib/active_record/connection_adapters/type/float.rb +++ b/activerecord/lib/active_record/connection_adapters/type/float.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Float < Value # :nodoc: + class Float < Value include Numeric def type @@ -12,6 +12,8 @@ module ActiveRecord ::Float end + alias type_cast_for_database type_cast + private def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/type/integer.rb b/activerecord/lib/active_record/connection_adapters/type/integer.rb index 8f3469434c..596f4de2a8 100644 --- a/activerecord/lib/active_record/connection_adapters/type/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/type/integer.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Integer < Value # :nodoc: + class Integer < Value include Numeric def type @@ -12,6 +12,8 @@ module ActiveRecord ::Fixnum end + alias type_cast_for_database type_cast + private def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/type/string.rb b/activerecord/lib/active_record/connection_adapters/type/string.rb index 55f0e1ee1c..471f949e09 100644 --- a/activerecord/lib/active_record/connection_adapters/type/string.rb +++ b/activerecord/lib/active_record/connection_adapters/type/string.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class String < Value # :nodoc: + class String < Value def type :string end diff --git a/activerecord/lib/active_record/connection_adapters/type/text.rb b/activerecord/lib/active_record/connection_adapters/type/text.rb index ee5842a3fc..61095ebb38 100644 --- a/activerecord/lib/active_record/connection_adapters/type/text.rb +++ b/activerecord/lib/active_record/connection_adapters/type/text.rb @@ -3,7 +3,7 @@ require 'active_record/connection_adapters/type/string' module ActiveRecord module ConnectionAdapters module Type - class Text < String # :nodoc: + class Text < String def type :text end diff --git a/activerecord/lib/active_record/connection_adapters/type/time.rb b/activerecord/lib/active_record/connection_adapters/type/time.rb index 4dd201e3fe..bc331b0fa7 100644 --- a/activerecord/lib/active_record/connection_adapters/type/time.rb +++ b/activerecord/lib/active_record/connection_adapters/type/time.rb @@ -1,7 +1,7 @@ module ActiveRecord module ConnectionAdapters module Type - class Time < Value # :nodoc: + class Time < Value include TimeValue def type diff --git a/activerecord/lib/active_record/connection_adapters/type/value.rb b/activerecord/lib/active_record/connection_adapters/type/value.rb index 54a3e9dd7a..60b443004c 100644 --- a/activerecord/lib/active_record/connection_adapters/type/value.rb +++ b/activerecord/lib/active_record/connection_adapters/type/value.rb @@ -1,9 +1,11 @@ module ActiveRecord module ConnectionAdapters module Type - class Value # :nodoc: + class Value 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] @@ -11,8 +13,13 @@ module ActiveRecord @limit = options[:limit] end + # The simplified that this object represents. Subclasses + # should 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 @@ -21,6 +28,10 @@ module ActiveRecord value end + def type_cast_for_database(value) + type_cast_for_write(value) + end + def text? false end @@ -39,7 +50,9 @@ module ActiveRecord private - def cast_value(value) + # 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 diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 4571cc0786..07eafef788 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -286,6 +286,8 @@ module ActiveRecord @new_record = false + self.class.define_attribute_methods + run_callbacks :find run_callbacks :initialize diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 71e176a328..05c4b13016 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -20,7 +20,7 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |counter_association| - has_many_association = reflect_on_association(counter_association.to_sym) + 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 } @@ -34,8 +34,7 @@ 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({ @@ -167,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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 47d32fae05..d40bea5ea7 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -649,7 +649,7 @@ 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 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index aa1166750f..a4e10ed2e7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -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 @@ -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 @@ -308,8 +294,6 @@ module ActiveRecord @arel_engine = nil @column_defaults = nil @column_names = nil - @columns = nil - @columns_hash = nil @column_types = nil @content_columns = nil @dynamic_methods_hash = nil diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 29ed499b1b..7dc7169a02 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) @@ -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/properties.rb b/activerecord/lib/active_record/properties.rb new file mode 100644 index 0000000000..39c39ad9ff --- /dev/null +++ b/activerecord/lib/active_record/properties.rb @@ -0,0 +1,107 @@ +module ActiveRecord + module Properties + extend ActiveSupport::Concern + + Type = ConnectionAdapters::Type + + 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. + # + # ==== 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) + name = name.to_s + user_provided_columns[name] = ConnectionAdapters::Column.new(name, nil, 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)).each do |column| + if Type::DecimalWithoutScale === column.cast_type + ActiveSupport::Deprecation.warn <<-MESSAGE.strip_heredoc + Decimal columns with 0 scale being automatically treated as integers + is deprecated, and will be removed in a future version of Rails. If + you'd like to keep this behavior, add + + property :#{column.name}, Type::Integer.new + + to your #{name} model. + MESSAGE + end + 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 reset_column_information # :nodoc: + super + + @columns = nil + @columns_hash = nil + end + + private + + def user_provided_columns + @user_provided_columns ||= {} + end + + def add_user_provided_columns(schema_columns) + schema_columns.reject { |column| + user_provided_columns.key? column.name + } + user_provided_columns.values + end + end + end +end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 0eec6774a0..dd80ec6274 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" @@ -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) @@ -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 diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 56cf9bcd27..d155517b18 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -277,7 +277,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass.reflect_on_association(group_attrs.first.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 group_fields = Array(associated ? association.foreign_key : group_attrs) else 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/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 a607e9ac87..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. 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..b6fccc9b94 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -47,7 +47,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 +74,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 diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 1f55cce352..972abf7cdc 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -122,7 +122,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_equal "Champs-Élysées", composite.address.street composite.address = FullAddress.new("Paris", "Rue Basse") - skip "Saving with custom OID type is currently not supported." composite.save! assert_equal 'Paris', composite.reload.address.city diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index b9e296ed8f..b6c6e38f62 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -129,7 +129,7 @@ class SchemaTest < ActiveRecord::TestCase SQL song = Song.create - album = Album.create + Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index b478db749d..13b754d226 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -60,7 +60,6 @@ class CopyTableTest < ActiveRecord::TestCase assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type assert_equal original_id.limit, copied_id.limit - assert_equal original_id.primary, copied_id.primary end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 0c4f06d6a9..8c9a051eea 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,10 +15,10 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - column = Struct.new(:type, :name).new(:string, "foo") + cast_type = Type::String.new binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, column) + assert_equal expected, @conn.type_cast(binary, cast_type) end def test_type_cast_symbol @@ -84,7 +84,7 @@ module ActiveRecord assert_raise(TypeError) { @conn.type_cast(obj, nil) } end - def test_quoted_id + def test_type_cast_object_which_responds_to_quoted_id quoted_id_obj = Class.new { def quoted_id "'zomg'" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ebcf453305..4bd4486b41 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1167,6 +1167,13 @@ class EagerAssociationTest < ActiveRecord::TestCase ) end + test "deep preload" do + post = Post.preload(author: :posts, comments: :post).first + + assert_predicate post.author.association(:posts), :loaded? + assert_predicate post.comments.first.association(:post), :loaded? + end + test "preloading does not cache has many association subset when preloaded with a through association" do author = Author.includes(:comments_with_order_and_conditions, :posts).first assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size } @@ -1196,6 +1203,15 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + test "preloading the same association twice works" do + Member.create! + members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a + assert_no_queries { + members_with_membership = members.select(&:current_membership) + assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size + } + end + test "preloading with a polymorphic association and using the existential predicate" do assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 878f1877db..8d8201ddae 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -70,6 +70,14 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base :foreign_key => "developer_id" end +class SubDeveloper < Developer + self.table_name = 'developers' + has_and_belongs_to_many :special_projects, + :join_table => 'developers_projects', + :foreign_key => "project_id", + :association_foreign_key => "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings @@ -814,7 +822,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [], Pirate.where(id: redbeard.id) end - test "has and belongs to many associations on new records use null relations" do + def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects assert_no_queries do assert_equal [], projects @@ -860,4 +868,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes magazine.articles, article end + + def test_redefine_habtm + child = SubDeveloper.new("name" => "Aredridel") + child.special_projects << SpecialProject.new("name" => "Special Project") + assert_equal true, child.save + end end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index b23517b2f9..07cf65a760 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -126,4 +126,14 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a assert_equal 2, categories.size end + + test "the correct records are loaded when including an aliased association" do + author = Author.create! name: "Jon" + author.categories.create! name: 'Not Special' + author.special_categories.create! name: 'Special' + + categories = author.categories.eager_load(:special_categorizations).order(:name).to_a + assert_equal 0, categories.first.special_categorizations.size + assert_equal 1, categories.second.special_categorizations.size + end end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7c7c1fbfbd..d65c4b0638 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -102,8 +102,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_columns_should_obey_set_primary_key - pk = Subscriber.columns.find { |x| x.name == 'nick' } - assert pk.primary, 'nick should be primary key' + pk = Subscriber.columns_hash[Subscriber.primary_key] + assert_equal 'nick', pk.name, 'nick should be primary key' end def test_primary_key_with_no_id @@ -518,6 +518,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find('1-meowmeow'), Topic.find(1) end + def test_find_by_slug_with_array + assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new assert_equal false, Topic.new == Topic.new @@ -980,6 +984,10 @@ class BasicsTest < ActiveRecord::TestCase class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end def test_big_decimal_conditions diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 40f73cd68c..0bc7ee6d64 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -21,7 +21,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection @subscriber = LogListener.new - @pk = Topic.columns.find { |c| c.primary } + @pk = Topic.columns_hash[Topic.primary_key] @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) end @@ -60,12 +60,10 @@ module ActiveRecord end def test_logs_bind_vars - pk = Topic.columns.find { |x| x.primary } - payload = { :name => 'SQL', :sql => 'select * from topics where id = ?', - :binds => [[pk, 10]] + :binds => [[@pk, 10]] } event = ActiveSupport::Notifications::Event.new( 'foo', @@ -87,7 +85,7 @@ module ActiveRecord }.new logger.sql event - assert_match([[pk.name, 10]].inspect, logger.debugs.first) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index b8de78934e..b9445ee072 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -15,6 +15,10 @@ Company.has_many :accounts class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' + + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new + property :atoms_in_universe, Type::Integer.new end class CalculationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb deleted file mode 100644 index 3257f5bed8..0000000000 --- a/activerecord/test/cases/column_test.rb +++ /dev/null @@ -1,159 +0,0 @@ -require "cases/helper" -require 'models/company' - -module ActiveRecord - module ConnectionAdapters - class ColumnTest < ActiveRecord::TestCase - def test_type_cast_boolean - column = Column.new("field", nil, Type::Boolean.new) - assert column.type_cast('').nil? - assert column.type_cast(nil).nil? - - assert column.type_cast(true) - assert column.type_cast(1) - assert column.type_cast('1') - assert column.type_cast('t') - assert column.type_cast('T') - assert column.type_cast('true') - assert column.type_cast('TRUE') - assert column.type_cast('on') - assert column.type_cast('ON') - - # explicitly check for false vs nil - assert_equal false, column.type_cast(false) - assert_equal false, column.type_cast(0) - assert_equal false, column.type_cast('0') - assert_equal false, column.type_cast('f') - assert_equal false, column.type_cast('F') - assert_equal false, column.type_cast('false') - assert_equal false, column.type_cast('FALSE') - assert_equal false, column.type_cast('off') - assert_equal false, column.type_cast('OFF') - assert_equal false, column.type_cast(' ') - assert_equal false, column.type_cast("\u3000\r\n") - assert_equal false, column.type_cast("\u0000") - assert_equal false, column.type_cast('SOMETHING RANDOM') - end - - def test_type_cast_string - column = Column.new("field", nil, Type::String.new) - assert_equal "1", column.type_cast(true) - assert_equal "0", column.type_cast(false) - assert_equal "123", column.type_cast(123) - end - - def test_type_cast_integer - column = Column.new("field", nil, Type::Integer.new) - assert_equal 1, column.type_cast(1) - assert_equal 1, column.type_cast('1') - assert_equal 1, column.type_cast('1ignore') - assert_equal 0, column.type_cast('bad1') - assert_equal 0, column.type_cast('bad') - assert_equal 1, column.type_cast(1.7) - assert_equal 0, column.type_cast(false) - assert_equal 1, column.type_cast(true) - assert_nil column.type_cast(nil) - end - - def test_type_cast_non_integer_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast([1,2]) - assert_nil column.type_cast({1 => 2}) - assert_nil column.type_cast((1..2)) - end - - def test_type_cast_activerecord_to_integer - column = Column.new("field", nil, Type::Integer.new) - firm = Firm.create(:name => 'Apple') - assert_nil column.type_cast(firm) - end - - def test_type_cast_object_without_to_i_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_nil column.type_cast(Float::NAN) - assert_nil column.type_cast(1.0/0.0) - end - - def test_type_cast_float - column = Column.new("field", nil, Type::Float.new) - assert_equal 1.0, column.type_cast("1") - end - - def test_type_cast_decimal - column = Column.new("field", nil, Type::Decimal.new) - assert_equal BigDecimal.new("0"), column.type_cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), column.type_cast(123.0) - assert_equal BigDecimal.new("1"), column.type_cast(:"1") - end - - def test_type_cast_binary - column = Column.new("field", nil, Type::Binary.new) - assert_equal nil, column.type_cast(nil) - assert_equal "1", column.type_cast("1") - assert_equal 1, column.type_cast(1) - end - - def test_type_cast_time - column = Column.new("field", nil, Type::Time.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, column.type_cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - column = Column.new("field", nil, Type::DateTime.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - column = Column.new("field", nil, Type::Date.new) - assert_equal nil, column.type_cast(nil) - assert_equal nil, column.type_cast('') - assert_equal nil, column.type_cast(' ') - assert_equal nil, column.type_cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, column.type_cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - column = Column.new("field", nil, Type::Integer.new) - assert_equal 1800, column.type_cast(30.minutes) - assert_equal 7200, column.type_cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :local].each do |zone| - with_timezone_config default: zone do - column = Column.new("field", nil, Type::DateTime.new) - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), column.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - if current_adapter?(:SQLite3Adapter) - def test_binary_encoding - column = Column.new("field", nil, SQLite3Binary.new) - utf8_string = "a string".encode(Encoding::UTF_8) - type_cast = column.type_cast(utf8_string) - - assert_equal Encoding::ASCII_8BIT, type_cast.encoding - end - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/type/type_map_test.rb b/activerecord/test/cases/connection_adapters/type/type_map_test.rb index 3abd7a276e..e85bbc8dc7 100644 --- a/activerecord/test/cases/connection_adapters/type/type_map_test.rb +++ b/activerecord/test/cases/connection_adapters/type/type_map_test.rb @@ -125,6 +125,16 @@ module ActiveRecord assert_equal mapping.lookup(3), 'string' assert_kind_of Type::Value, mapping.lookup(4) end + + def test_clear_mappings + time = Time.new + mapping = TypeMap.new + + mapping.register_type(/time/i, time) + mapping.clear + + assert_not_equal mapping.lookup('time'), time + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 18df30faf5..3958c3bfff 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -77,12 +77,16 @@ module ActiveRecord assert_lookup_type :integer, 'tinyint' assert_lookup_type :integer, 'smallint' assert_lookup_type :integer, 'bigint' - assert_lookup_type :integer, 'decimal(2)' - assert_lookup_type :integer, 'decimal(2,0)' - assert_lookup_type :integer, 'numeric(2)' - assert_lookup_type :integer, 'numeric(2,0)' - assert_lookup_type :integer, 'number(2)' - assert_lookup_type :integer, 'number(2,0)' + end + + def test_decimal_without_scale + types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal :decimal, cast_type.type + assert_equal 2, cast_type.type_cast(2.1) + end end private diff --git a/activerecord/test/cases/custom_properties_test.rb b/activerecord/test/cases/custom_properties_test.rb new file mode 100644 index 0000000000..9598f0299c --- /dev/null +++ b/activerecord/test/cases/custom_properties_test.rb @@ -0,0 +1,64 @@ +require 'cases/helper' + +class OverloadedType < ActiveRecord::Base + property :overloaded_float, Type::Integer.new + property :overloaded_string_with_limit, Type::String.new(limit: 50) + property :non_existent_decimal, Type::Decimal.new +end + +class UnoverloadedType < ActiveRecord::Base + self.table_name = 'overloaded_types' +end + +module ActiveRecord + class CustomPropertiesTest < ActiveRecord::TestCase + def test_overloading_types + data = OverloadedType.new + + data.overloaded_float = "1.1" + data.unoverloaded_float = "1.1" + + assert_equal 1, data.overloaded_float + assert_equal 1.1, data.unoverloaded_float + end + + def test_overloaded_properties_save + data = OverloadedType.new + + data.overloaded_float = "2.2" + data.save! + data.reload + + assert_equal 2, data.overloaded_float + assert_equal 2.0, UnoverloadedType.last.overloaded_float + end + + def test_properties_assigned_in_constructor + data = OverloadedType.new(overloaded_float: '3.3') + + assert_equal 3, data.overloaded_float + end + + def test_overloaded_properties_with_limit + assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit + assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit + end + + def test_nonexistent_property + data = OverloadedType.new(non_existent_decimal: 1) + + assert_equal BigDecimal.new(1), data.non_existent_decimal + assert_raise ActiveRecord::UnknownAttributeError do + UnoverloadedType.new(non_existent_decimal: 1) + end + end + + def test_overloaded_properties_have_no_default + data = OverloadedType.new + unoverloaded_data = UnoverloadedType.new + + assert_nil data.overloaded_float + assert unoverloaded_data.overloaded_float + end + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index eaf2cada9d..937646b09a 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -9,6 +9,7 @@ require 'active_record' require 'cases/test_case' require 'active_support/dependencies' require 'active_support/logger' +require 'active_support/core_ext/string/strip' require 'support/config' require 'support/connection' diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 455ec78f68..aa679d4a35 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -11,7 +11,13 @@ require MIGRATIONS_ROOT + "/rename/1_we_need_things" require MIGRATIONS_ROOT + "/rename/2_rename_things" require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers" -class BigNumber < ActiveRecord::Base; end +class BigNumber < ActiveRecord::Base + unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) + property :value_of_e, Type::Integer.new + end + property :world_population, Type::Integer.new + property :my_house_population, Type::Integer.new +end class Reminder < ActiveRecord::Base; end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 56d0dd6a77..c719918fd7 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -149,38 +149,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key end - def test_two_models_with_same_table_but_different_primary_key - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - k1.primary_key = 'id' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - k2.primary_key = 'title' - - assert k1.columns.find { |c| c.name == 'id' }.primary - assert !k1.columns.find { |c| c.name == 'title' }.primary - assert k1.columns_hash['id'].primary - assert !k1.columns_hash['title'].primary - - assert !k2.columns.find { |c| c.name == 'id' }.primary - assert k2.columns.find { |c| c.name == 'title' }.primary - assert !k2.columns_hash['id'].primary - assert k2.columns_hash['title'].primary - end - - def test_models_with_same_table_have_different_columns - k1 = Class.new(ActiveRecord::Base) - k1.table_name = 'posts' - - k2 = Class.new(ActiveRecord::Base) - k2.table_name = 'posts' - - k1.columns.zip(k2.columns).each do |col1, col2| - assert !col1.equal?(col2) - end - end - def test_auto_detect_primary_key_from_schema MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index e2439b9a24..bbd5298da1 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -3,14 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class QuotingTest < ActiveRecord::TestCase - class FakeColumn < ActiveRecord::ConnectionAdapters::Column - attr_accessor :type - - def initialize type - @type = type - end - end - def setup @quoter = Class.new { include Quoting }.new end @@ -101,12 +93,12 @@ module ActiveRecord def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer)) + assert_equal '1', @quoter.quote(true, Type::Integer.new) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer)) + assert_equal '0', @quoter.quote(false, Type::Integer.new) end def test_quote_float @@ -166,26 +158,26 @@ module ActiveRecord end def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer)) - assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer)) + assert_equal "1", @quoter.quote('1', Type::Integer.new) + assert_equal "1", @quoter.quote('1.2', Type::Integer.new) end def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float)) - assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float)) + assert_equal "1.0", @quoter.quote('1', Type::Float.new) + assert_equal "1.2", @quoter.quote('1.2', Type::Float.new) end def test_quote_as_mb_chars_binary_column string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary)) + assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new) end def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new) end def test_string_with_crazy_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo)) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end def test_quote_duration @@ -193,7 +185,7 @@ module ActiveRecord end def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer)) + assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new) end end end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c085fcf161..e6603f28be 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -200,7 +200,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_should_not_raise_error_when_compared_to_other_object - assert_nothing_raised { Firm.reflections['clients'] == Object.new } + assert_not_equal Object.new, Firm._reflections['clients'] + end + + def test_has_and_belongs_to_many_reflection + assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro + assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name end def test_has_many_through_reflection diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index c6decaad89..b9e69bdb08 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual assert_equal 'ruby on rails', bind.last end - + def test_rewhere_with_one_condition relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone') diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 4b146c11bc..88df997a2f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -552,13 +552,6 @@ class RelationTest < ActiveRecord::TestCase end end - def test_deep_preload - post = Post.preload(author: :posts, comments: :post).first - - assert_predicate post.author.association(:posts), :loaded? - assert_predicate post.comments.first.association(:post), :loaded? - end - def test_preload_applies_to_all_chained_preloaded_scopes assert_queries(3) do post = Post.with_comments.with_tags.first @@ -750,6 +743,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal [], relation.to_a end + def test_typecasting_where_with_array + ids = Author.pluck(:id) + slugs = ids.map { |id| "#{id}-as-a-slug" } + + assert_equal Author.all.to_a, Author.where(id: slugs).to_a + end + def test_find_all_using_where_with_relation david = authors(:david) # switching the lines below would succeed in current rails diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 61bca976f7..9602252b2e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,10 +1,8 @@ require "cases/helper" class SchemaDumperTest < ActiveRecord::TestCase - def setup - super + setup do ActiveRecord::SchemaMigration.create_table - @stream = StringIO.new end def standard_dump @@ -25,7 +23,8 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_magic_comment - assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + output = standard_dump + assert_match "# encoding: #{@stream.external_encoding.name}", output end def test_schema_dump diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index c46060a646..7dd1f10ce9 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -1,8 +1,11 @@ require "cases/helper" require 'models/contact' require 'models/topic' +require 'models/book' class SerializationTest < ActiveRecord::TestCase + fixtures :books + FORMATS = [ :xml, :json ] def setup @@ -65,4 +68,20 @@ class SerializationTest < ActiveRecord::TestCase ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end + + def test_read_attribute_for_serialization_with_format_after_init + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.new(format: 'paperback') + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end + + def test_read_attribute_for_serialization_with_format_after_find + klazz = Class.new(ActiveRecord::Base) + klazz.table_name = 'books' + + book = klazz.find(books(:awdr).id) + assert_equal 'paperback', book.read_attribute_for_serialization(:format) + end end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb new file mode 100644 index 0000000000..5d5f442d3a --- /dev/null +++ b/activerecord/test/cases/types_test.rb @@ -0,0 +1,159 @@ +require "cases/helper" +require 'models/company' + +module ActiveRecord + module ConnectionAdapters + class TypesTest < ActiveRecord::TestCase + def test_type_cast_boolean + type = Type::Boolean.new + assert type.type_cast('').nil? + assert type.type_cast(nil).nil? + + assert type.type_cast(true) + assert type.type_cast(1) + assert type.type_cast('1') + assert type.type_cast('t') + assert type.type_cast('T') + assert type.type_cast('true') + assert type.type_cast('TRUE') + assert type.type_cast('on') + assert type.type_cast('ON') + + # explicitly check for false vs nil + assert_equal false, type.type_cast(false) + assert_equal false, type.type_cast(0) + assert_equal false, type.type_cast('0') + assert_equal false, type.type_cast('f') + assert_equal false, type.type_cast('F') + assert_equal false, type.type_cast('false') + assert_equal false, type.type_cast('FALSE') + assert_equal false, type.type_cast('off') + assert_equal false, type.type_cast('OFF') + assert_equal false, type.type_cast(' ') + assert_equal false, type.type_cast("\u3000\r\n") + assert_equal false, type.type_cast("\u0000") + assert_equal false, type.type_cast('SOMETHING RANDOM') + end + + def test_type_cast_string + type = Type::String.new + assert_equal "1", type.type_cast(true) + assert_equal "0", type.type_cast(false) + assert_equal "123", type.type_cast(123) + end + + def test_type_cast_integer + type = Type::Integer.new + assert_equal 1, type.type_cast(1) + assert_equal 1, type.type_cast('1') + assert_equal 1, type.type_cast('1ignore') + assert_equal 0, type.type_cast('bad1') + assert_equal 0, type.type_cast('bad') + assert_equal 1, type.type_cast(1.7) + assert_equal 0, type.type_cast(false) + assert_equal 1, type.type_cast(true) + assert_nil type.type_cast(nil) + end + + def test_type_cast_non_integer_to_integer + type = Type::Integer.new + assert_nil type.type_cast([1,2]) + assert_nil type.type_cast({1 => 2}) + assert_nil type.type_cast((1..2)) + end + + def test_type_cast_activerecord_to_integer + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast(firm) + end + + def test_type_cast_object_without_to_i_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Object.new) + end + + def test_type_cast_nan_and_infinity_to_integer + type = Type::Integer.new + assert_nil type.type_cast(Float::NAN) + assert_nil type.type_cast(1.0/0.0) + end + + def test_type_cast_float + type = Type::Float.new + assert_equal 1.0, type.type_cast("1") + end + + def test_type_cast_decimal + type = Type::Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast(123.0) + assert_equal BigDecimal.new("1"), type.type_cast(:"1") + end + + def test_type_cast_binary + type = Type::Binary.new + assert_equal nil, type.type_cast(nil) + assert_equal "1", type.type_cast("1") + assert_equal 1, type.type_cast(1) + end + + def test_type_cast_time + type = Type::Time.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast('ABC') + + time_string = Time.now.utc.strftime("%T") + assert_equal time_string, type.type_cast(time_string).strftime("%T") + end + + def test_type_cast_datetime_and_timestamp + type = Type::DateTime.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + datetime_string = Time.now.utc.strftime("%FT%T") + assert_equal datetime_string, type.type_cast(datetime_string).strftime("%FT%T") + end + + def test_type_cast_date + type = Type::Date.new + assert_equal nil, type.type_cast(nil) + assert_equal nil, type.type_cast('') + assert_equal nil, type.type_cast(' ') + assert_equal nil, type.type_cast('ABC') + + date_string = Time.now.utc.strftime("%F") + assert_equal date_string, type.type_cast(date_string).strftime("%F") + end + + def test_type_cast_duration_to_integer + type = Type::Integer.new + assert_equal 1800, type.type_cast(30.minutes) + assert_equal 7200, type.type_cast(2.hours) + end + + def test_string_to_time_with_timezone + [:utc, :local].each do |zone| + with_timezone_config default: zone do + type = Type::DateTime.new + assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast("Wed, 04 Sep 2013 03:00:00 EAT") + end + end + end + + if current_adapter?(:SQLite3Adapter) + def test_binary_encoding + type = SQLite3Binary.new + utf8_string = "a string".encode(Encoding::UTF_8) + type_cast = type.type_cast(utf8_string) + + assert_equal Encoding::ASCII_8BIT, type_cast.encoding + end + end + end + end +end diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index fb48645456..abe56752c6 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -2,8 +2,10 @@ awdr: author_id: 1 id: 1 name: "Agile Web Development with Rails" + format: "paperback" rfr: author_id: 1 id: 2 name: "Ruby for Rails" + format: "ebook" diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 8c52ad2724..4cce58f4f4 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -103,6 +103,7 @@ ActiveRecord::Schema.define do create_table :books, force: true do |t| t.integer :author_id + t.string :format t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 @@ -854,6 +855,12 @@ ActiveRecord::Schema.define do execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" end + + create_table :overloaded_types, force: true do |t| + t.float :overloaded_float, default: 500 + t.float :unoverloaded_float + t.string :overloaded_string_with_limit, limit: 255 + end end Course.connection.create_table :courses, force: true do |t| |