diff options
Diffstat (limited to 'activerecord')
72 files changed, 1368 insertions, 798 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 42b8515133..0841b2f679 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,75 @@ +* Uniqueness validator raises descriptive error when running on a persisted + record without primary key. + + Closes #21304. + + *Yves Senn* + +* Add a native JSON data type support in MySQL. + + Example: + + create_table :json_data_type do |t| + t.json :settings + end + + *Ryuta Kamizono* + +* Descriptive error message when fixtures contain a missing column. + + Closes #21201. + + *Yves Senn* + +* `ActiveRecord::Tasks::PostgreSQLDatabaseTasks` fail if shellout to + postgresql commands (like `pg_dump`) is not successful. + + *Bryan Paxton*, *Nate Berkopec* + +* Add `ActiveRecord::Relation#in_batches` to work with records and relations + in batches. + + Available options are `of` (batch size), `load`, `begin_at`, and `end_at`. + + Examples: + + Person.in_batches.each_record(&:party_all_night!) + Person.in_batches.update_all(awesome: true) + Person.in_batches.delete_all + Person.in_batches.each do |relation| + relation.delete_all + sleep 10 # Throttles the delete queries + end + + Closes #20933. + + *Sina Siadat* + +* Added methods for PostgreSQL geometric data types to use in migrations + + Example: + + create_table :foo do |t| + t.line :foo_line + t.lseg :foo_lseg + t.box :foo_box + t.path :foo_path + t.polygon :foo_polygon + t.circle :foo_circle + end + + *Mehmet Emin İNAÇ* + +* Add `cache_key` to ActiveRecord::Relation. + + Example: + + @users = User.where("name like ?", "%Alberto%") + @users.cache_key + => "/users/query-5942b155a43b139f2471b872ac54251f-3-20150714212107656125000" + + *Alberto Fernández-Capel* + * Properly allow uniqueness validations on primary keys. Fixes #20966. @@ -301,6 +373,10 @@ *Ryuta Kamizono* +* Remove `ActiveRecord::Serialization::XmlSerializer` from core. + + *Zachary Scott* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index f5cf92db64..264f869c68 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -53,6 +53,7 @@ module ActiveRecord autoload :Persistence autoload :QueryCache autoload :Querying + autoload :CollectionCacheKey autoload :ReadonlyAttributes autoload :RecordInvalid, 'active_record/validations' autoload :Reflection diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 7c729676a7..c7b396f3d4 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -251,6 +251,14 @@ module ActiveRecord initialize_attributes(record) end end + + # Returns true if statement cache should be skipped on the association reader. + def skip_statement_cache? + reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.scope_attributes? || + reflection.source_reflection.active_record.default_scopes.any? + end end end end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index ffd9c9d6fc..b18d99d54e 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 @@ -46,7 +46,7 @@ module ActiveRecord::Associations::Builder join_model = Class.new(ActiveRecord::Base) { class << self; - attr_accessor :class_resolver + attr_accessor :left_model attr_accessor :name attr_accessor :table_name_resolver attr_accessor :left_reflection @@ -58,7 +58,7 @@ module ActiveRecord::Associations::Builder end def self.compute_type(class_name) - class_resolver.compute_type class_name + left_model.compute_type class_name end def self.add_left_association(name, options) @@ -72,11 +72,15 @@ module ActiveRecord::Associations::Builder self.right_reflection = _reflect_on_association(rhs_name) end + def self.retrieve_connection + left_model.retrieve_connection + end + } join_model.name = "HABTM_#{association_name.to_s.camelize}" join_model.table_name_resolver = habtm - join_model.class_resolver = lhs_model + join_model.left_model = lhs_model join_model.add_left_association :left_side, anonymous_class: lhs_model join_model.add_right_association association_name, belongs_to_options(options) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 87576abd92..256df3ca11 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -62,8 +62,10 @@ module ActiveRecord record.send(reflection.association_primary_key) end else - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) + @association_ids ||= ( + column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" + scope.pluck(column) + ) end end @@ -440,12 +442,7 @@ module ActiveRecord private def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.to_a - end + return scope.to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index b5a8c81fe4..29c711082a 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -227,6 +227,31 @@ module ActiveRecord @association.last(*args) end + # Gives a record (or N records if a parameter is supplied) from the collection + # using the same rules as <tt>ActiveRecord::Base.take</tt>. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1>, + # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> + # # ] + # + # person.pets.take # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1> + # + # person.pets.take(2) + # # => [ + # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, + # # #<Pet id: 2, name: "Spook", person_id: 1> + # # ] + # + # another_person_without.pets # => [] + # another_person_without.pets.take # => nil + # another_person_without.pets.take(2) # => [] def take(n = nil) @association.take(n) end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 9f6c832c1b..19cdd7f470 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -87,8 +87,15 @@ module ActiveRecord [association_scope.limit_value, count].compact.min end + + # Returns whether a counter cache should be used for this association. + # + # The counter_cache option must be given on either the owner or inverse + # association, and the column must be present on the owner. def has_cached_counter?(reflection = reflection()) - owner.attribute_present?(cached_counter_attribute_name(reflection)) + if reflection.options[:counter_cache] || (inverse = inverse_which_updates_counter_cache(reflection)) && inverse.options[:counter_cache] + owner.attribute_present?(cached_counter_attribute_name(reflection)) + end end def cached_counter_attribute_name(reflection = reflection()) diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 6ecc741195..3992a240b9 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -116,7 +116,7 @@ module ActiveRecord when String preloaders_for_one(association.to_sym, records, scope) else - raise ArgumentError, "#{association.inspect} was not recognised for preload" + raise ArgumentError, "#{association.inspect} was not recognized for preload" end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 30c5d72482..03cb8cb8c3 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -47,12 +47,7 @@ module ActiveRecord end def get_records - if reflection.scope_chain.any?(&:any?) || - scope.eager_loading? || - klass.scope_attributes? - - return scope.limit(1).to_a - end + return scope.limit(1).to_a if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 7fb899c242..0862306749 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -385,27 +385,27 @@ module ActiveRecord # # For example: # - # class PostsController < ActionController::Base - # after_action :print_accessed_fields, only: :index + # class PostsController < ActionController::Base + # after_action :print_accessed_fields, only: :index # - # def index - # @posts = Post.all - # end + # def index + # @posts = Post.all + # end # - # private + # private # - # def print_accessed_fields - # p @posts.first.accessed_fields + # def print_accessed_fields + # p @posts.first.accessed_fields + # end # end - # end # # Which allows you to quickly change your code to: # - # class PostsController < ActionController::Base - # def index - # @posts = Post.select(:id, :title, :author_id, :updated_at) + # class PostsController < ActionController::Base + # def index + # @posts = Post.select(:id, :title, :author_id, :updated_at) + # end # end - # end def accessed_fields @attributes.accessed end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index e03bf5945d..60eecab0d0 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -11,7 +11,7 @@ module ActiveRecord # serialized object must be of that class on assignment and retrieval. # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # - # Empty objects as +{}+, in the case of +Hash+, or +[]+, in the case of + # Empty objects as <tt>{}</tt>, in the case of +Hash+, or <tt>[]</tt>, in the case of # +Array+, will always be persisted as null. # # Keep in mind that database adapters handle certain serialization tasks diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index dbb0e2fab2..d0de42d27c 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -234,7 +234,7 @@ module ActiveRecord super end - # Marks this record to be destroyed as part of the parents save transaction. + # Marks this record to be destroyed as part of the parent's save transaction. # This does _not_ actually destroy the record instantly, rather child record will be destroyed # when <tt>parent.save</tt> is called. # @@ -243,7 +243,7 @@ module ActiveRecord @marked_for_destruction = true end - # Returns whether or not this record will be destroyed as part of the parents save transaction. + # Returns whether or not this record will be destroyed as part of the parent's save transaction. # # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model. def marked_for_destruction? diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c918e88590..55a7e053bc 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -280,6 +280,7 @@ module ActiveRecord #:nodoc: extend Explain extend Enum extend Delegation::DelegateCache + extend CollectionCacheKey include Core include Persistence diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index c7c769b283..ccdbebbc77 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -206,7 +206,8 @@ module ActiveRecord # == Ordering callbacks # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ - # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option. + # callback (+log_children+ in this case) should be executed before the children get destroyed by the + # <tt>dependent: destroy</tt> option. # # Let's look at the code below: # diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb new file mode 100644 index 0000000000..3c4ca3d116 --- /dev/null +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -0,0 +1,31 @@ +module ActiveRecord + module CollectionCacheKey + + def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc: + query_signature = Digest::MD5.hexdigest(collection.to_sql) + key = "#{collection.model_name.cache_key}/query-#{query_signature}" + + if collection.loaded? + size = collection.size + timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + else + column_type = type_for_attribute(timestamp_column.to_s) + column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + + query = collection + .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") + .unscope(:order) + result = connection.select_one(query) + + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + + if timestamp + "#{key}-#{size}-#{timestamp.utc.to_s(cache_timestamp_format)}" + else + "#{key}-#{size}" + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 38dd9578fe..1e13b24867 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -289,8 +289,12 @@ module ActiveRecord columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| - type = lookup_cast_type_from_column(columns[name]) - Relation::QueryAttribute.new(name, value, type) + if column = columns[name] + type = lookup_cast_type_from_column(column) + Relation::QueryAttribute.new(name, value, type) + else + raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".) + end end key_list = fixture.keys.map { |name| quote_column_name(name) } value_list = prepare_binds_for_database(binds).map do |value| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index d17e272ed1..3115e03ea2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -214,6 +214,7 @@ module ActiveRecord @name = name end + # Returns an array of ColumnDefinition objects for the columns of the table. def columns; @columns_hash.values; end # Returns a ColumnDefinition for the column with name +name+. @@ -369,6 +370,8 @@ module ActiveRecord self end + # remove the column +name+ from the table. + # remove_column(:account_id) def remove_column(name) @columns_hash.delete name.to_s end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 56227ddd80..ed14c781c6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -266,6 +266,11 @@ module ActiveRecord false end + # Does this adapter support json data type? + def supports_json? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) 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 af156c9c78..b3e55a0b90 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -10,6 +10,10 @@ module ActiveRecord options[:auto_increment] = true if type == :bigint super end + + def json(*args, **options) + args.each { |name| column(name, :json, options) } + end end class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition @@ -242,17 +246,19 @@ module ActiveRecord QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) auto_increment PRIMARY KEY", - :string => { :name => "varchar", :limit => 255 }, - :text => { :name => "text" }, - :integer => { :name => "int", :limit => 4 }, - :float => { :name => "float" }, - :decimal => { :name => "decimal" }, - :datetime => { :name => "datetime" }, - :time => { :name => "time" }, - :date => { :name => "date" }, - :binary => { :name => "blob" }, - :boolean => { :name => "tinyint", :limit => 1 } + primary_key: "int(11) auto_increment PRIMARY KEY", + string: { name: "varchar", limit: 255 }, + text: { name: "text" }, + integer: { name: "int", limit: 4 }, + float: { name: "float" }, + decimal: { name: "decimal" }, + datetime: { name: "datetime" }, + time: { name: "time" }, + date: { name: "date" }, + binary: { name: "blob" }, + boolean: { name: "tinyint", limit: 1 }, + bigint: { name: "bigint" }, + json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] @@ -790,6 +796,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + m.register_type %r(^json)i, MysqlJson.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -1043,6 +1050,14 @@ module ActiveRecord end end + class MysqlJson < Type::Internal::AbstractJson # :nodoc: + def changed_in_place?(raw_old_value, new_value) + # Normalization is required because MySQL JSON data format includes + # the space between the elements. + super(serialize(deserialize(raw_old_value)), new_value) + end + end + class MysqlString < Type::String # :nodoc: def serialize(value) case value @@ -1063,6 +1078,8 @@ module ActiveRecord end end + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql) + ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index e97e82f056..ff43c7ec42 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -41,6 +41,10 @@ module ActiveRecord true end + def supports_json? + version >= '5.7.8' + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: @@ -254,7 +258,7 @@ module ActiveRecord end def full_version - @full_version ||= @connection.info[:version] + @full_version ||= @connection.server_info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index 8e1256baad..dbc879ffd4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,32 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value # :nodoc: - include Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.is_a?(::Array) || value.is_a?(::Hash) - ::ActiveSupport::JSON.encode(value) - else - value - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end + class Json < Type::Internal::AbstractJson end end 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 index 022dbdfa27..6399bddbee 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -103,6 +103,30 @@ module ActiveRecord args.each { |name| column(name, :point, options) } end + def line(*args, **options) + args.each { |name| column(name, :line, options) } + end + + def lseg(*args, **options) + args.each { |name| column(name, :lseg, options) } + end + + def box(*args, **options) + args.each { |name| column(name, :box, options) } + end + + def path(*args, **options) + args.each { |name| column(name, :path, options) } + end + + def polygon(*args, **options) + args.each { |name| column(name, :polygon, options) } + end + + def circle(*args, **options) + args.each { |name| column(name, :circle, options) } + end + def serial(*args, **options) args.each { |name| column(name, :serial, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 2c43c46a3d..27291bd2ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -201,6 +201,10 @@ module ActiveRecord true end + def supports_json? + postgresql_version >= 90200 + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index d589620f8a..718f04871d 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -218,11 +218,12 @@ module ActiveRecord class UnknownPrimaryKey < ActiveRecordError attr_reader :model - def initialize(model) - super("Unknown primary key for table #{model.table_name} in model #{model}.") + def initialize(model, description = nil) + message = "Unknown primary key for table #{model.table_name} in model #{model}." + message += "\n#{description}" if description + super(message) @model = model end - end # Raised when a relation cannot be mutated because it's already loaded. diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4cfda302ea..04c6124eef 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -376,6 +376,7 @@ module ActiveRecord attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) end @@ -408,7 +409,10 @@ module ActiveRecord new.migrate direction end - # Disable DDL transactions for this migration. + # Disable the transaction wrapping this migration. + # You can still create your own transactions even after calling #disable_ddl_transaction! + # + # For more details read the {"Transactional Migrations" section above}[rdoc-ref:Migration]. def disable_ddl_transaction! @disable_ddl_transaction = true end @@ -716,7 +720,9 @@ module ActiveRecord end end - def table_name_options(config = ActiveRecord::Base) + # Builds a hash for use in ActiveRecord::Migration#proper_table_name using + # the Active Record object's table_name prefix and suffix + def table_name_options(config = ActiveRecord::Base) #:nodoc: { table_name_prefix: config.table_name_prefix, table_name_suffix: config.table_name_suffix @@ -808,7 +814,7 @@ module ActiveRecord new(:up, migrations, target_version).migrate end - def down(migrations_paths, target_version = nil, &block) + def down(migrations_paths, target_version = nil) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 4e597590e9..87a1988f2f 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -6,7 +6,7 @@ module ActiveRecord delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all - delegate :find_each, :find_in_batches, to: :all + delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :or, :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index da6b8447d3..6dd54f9262 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -16,11 +16,11 @@ module ActiveRecord config.app_generators.orm :active_record, :migration => true, :timestamps => true - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::QueryCache" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::QueryCache - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::ConnectionAdapters::ConnectionManagement" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::ConnectionAdapters::ConnectionManagement config.action_dispatch.rescue_responses.merge!( 'ActiveRecord::RecordNotFound' => :not_found, @@ -78,8 +78,8 @@ module ActiveRecord initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load - config.app_middleware.insert_after "::ActionDispatch::Callbacks", - "ActiveRecord::Migration::CheckPending" + config.app_middleware.insert_after ::ActionDispatch::Callbacks, + ActiveRecord::Migration::CheckPending end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index e4df122af6..e47b7b1ed9 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -298,6 +298,32 @@ module ActiveRecord limit_value ? to_a.many? : size > 1 end + # Returns a cache key that can be used to identify the records fetched by + # this query. The cache key is built with a fingerprint of the sql query, + # the number of records matched by the query and a timestamp of the last + # updated record. When a new record comes to match the query, or any of + # the existing records is updated or deleted, the cache key changes. + # + # Product.where("name like ?", "%Cosmic Encounter%").cache_key + # => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000" + # + # If the collection is loaded, the method will iterate through the records + # to generate the timestamp, otherwise it will trigger one SQL query like: + # + # SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') + # + # You can also pass a custom timestamp column to fetch the timestamp of the + # last updated record. + # + # Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at) + # + # You can customize the strategy to generate the key on a per model basis + # overriding ActiveRecord::Base#collection_cache_key. + def cache_key(timestamp_column = :updated_at) + @cache_keys ||= {} + @cache_keys[timestamp_column] ||= @klass.collection_cache_key(self, timestamp_column) + end + # Scope all queries to the current scope. # # Comment.where(post_id: 1).scoping do @@ -641,6 +667,13 @@ module ActiveRecord "#<#{self.class.name} [#{entries.join(', ')}]>" end + protected + + def load_records(records) + @records = records + @loaded = true + end + private def exec_queries diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index e07580a563..beb8fa511c 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,3 +1,5 @@ +require "active_record/relation/batches/batch_enumerator" + module ActiveRecord module Batches # Looping through a collection of records from the database @@ -122,24 +124,102 @@ module ActiveRecord end end + in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + yield batch.to_a + end + end + + # Yields ActiveRecord::Relation objects to work with a batch of records. + # + # Person.where("age > 21").in_batches do |relation| + # relation.delete_all + # sleep(10) # Throttle the delete queries + # end + # + # If you do not provide a block to #in_batches, it will return a + # BatchEnumerator which is enumerable. + # + # Person.in_batches.with_index do |relation, batch_index| + # puts "Processing relation ##{batch_index}" + # relation.each { |relation| relation.delete_all } + # end + # + # Examples of calling methods on the returned BatchEnumerator object: + # + # Person.in_batches.delete_all + # Person.in_batches.update_all(awesome: true) + # Person.in_batches.each_record(&:party_all_night!) + # + # ==== Options + # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. + # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # + # This is especially useful if you want to work with the + # ActiveRecord::Relation object instead of the array of records, or if + # you want multiple workers dealing with the same processing queue. You can + # make worker 1 handle all the records between id 0 and 10,000 and worker 2 + # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # option on each worker). + # + # # Let's process the next 2000 records + # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # + # An example of calling where query method on the relation: + # + # Person.in_batches.each do |relation| + # relation.update_all('age = age + 1') + # relation.where('age > 21').update_all(should_party: true) + # relation.where('age <= 21').delete_all + # end + # + # NOTE: If you are going to iterate through each record, you should call + # #each_record on the yielded BatchEnumerator: + # + # Person.in_batches.each_record(&:party_all_night!) + # + # NOTE: It's not possible to set the order. That is automatically set to + # ascending on the primary key ("id ASC") to make the batch ordering + # consistent. Therefore the primary key must be orderable, e.g an integer + # or a string. + # + # NOTE: You can't set the limit either, that's used to control the batch + # sizes. + def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + relation = self + unless block_given? + return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + end + if logger && (arel.orders.present? || arel.taken.present?) logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - relation = relation.reorder(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(of) relation = apply_limits(relation, begin_at, end_at) - records = relation.to_a + batch_relation = relation + + loop do + if load + records = batch_relation.to_a + ids = records.map(&:id) + yielded_relation = self.where(primary_key => ids) + yielded_relation.load_records(records) + else + ids = batch_relation.pluck(primary_key) + yielded_relation = self.where(primary_key => ids) + end - while records.any? - records_size = records.size - primary_key_offset = records.last.id - raise "Primary key not included in the custom select clause" unless primary_key_offset + break if ids.empty? - yield records + primary_key_offset = ids.last + raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset - break if records_size < batch_size + yield yielded_relation - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a + break if ids.length < of + batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) end end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb new file mode 100644 index 0000000000..153aae9584 --- /dev/null +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -0,0 +1,67 @@ +module ActiveRecord + module Batches + class BatchEnumerator + include Enumerable + + def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + @of = of + @relation = relation + @begin_at = begin_at + @end_at = end_at + end + + # Looping through a collection of records from the database (using the + # +all+ method, for example) is very inefficient since it will try to + # instantiate all the objects at once. + # + # In that case, batch processing methods allow you to work with the + # records in batches, thereby greatly reducing memory consumption. + # + # Person.in_batches.each_record do |person| + # person.do_awesome_stuff + # end + # + # Person.where("age > 21").in_batches(of: 10).each_record do |person| + # person.party_all_night! + # end + # + # If you do not provide a block to #each_record, it will return an Enumerator + # for chaining with other methods: + # + # Person.in_batches.each_record.with_index do |person, index| + # person.award_trophy(index + 1) + # end + def each_record + return to_enum(:each_record) unless block_given? + + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| + relation.to_a.each { |record| yield record } + end + end + + # Delegates #delete_all, #update_all, #destroy_all methods to each batch. + # + # People.in_batches.delete_all + # People.in_batches.destroy_all('age < 10') + # People.in_batches.update_all('age = age + 1') + [:delete_all, :update_all, :destroy_all].each do |method| + define_method(method) do |*args, &block| + @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + relation.send(method, *args, &block) + end + end + end + + # Yields an ActiveRecord::Relation object for each batch of records. + # + # Person.in_batches.each do |relation| + # relation.update_all(awesome: true) + # end + def each + enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + return enum.each { |relation| yield relation } if block_given? + enum + end + end + end +end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 3590b8846e..a1adf8e3ee 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -100,6 +100,7 @@ module ActiveRecord end def build_default_scope(base_rel = relation) # :nodoc: + return if abstract_class? if !Base.is_a?(method(:default_scope).owner) # The user has defined their own default scope method, so call that evaluate_default_scope { default_scope } diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index 48c12dcf9f..23dc6465af 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -18,5 +18,3 @@ module ActiveRecord #:nodoc: end end end - -require 'active_record/serializers/xml_serializer' diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb deleted file mode 100644 index 89b7e0be82..0000000000 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ /dev/null @@ -1,193 +0,0 @@ -require 'active_support/core_ext/hash/conversions' - -module ActiveRecord #:nodoc: - module Serialization - include ActiveModel::Serializers::Xml - - # Builds an XML document to represent the model. Some configuration is - # available through +options+. However more complicated cases should - # override ActiveRecord::Base#to_xml. - # - # By default the generated XML document will include the processing - # instruction and all the object's attributes. For example: - # - # <?xml version="1.0" encoding="UTF-8"?> - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <id type="integer">1</id> - # <approved type="boolean">false</approved> - # <replies-count type="integer">0</replies-count> - # <bonus-time type="dateTime">2000-01-01T08:28:00+12:00</bonus-time> - # <written-on type="dateTime">2003-07-16T09:28:00+1200</written-on> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # This behavior can be controlled with <tt>:only</tt>, <tt>:except</tt>, - # <tt>:skip_instruct</tt>, <tt>:skip_types</tt>, <tt>:dasherize</tt> and <tt>:camelize</tt> . - # The <tt>:only</tt> and <tt>:except</tt> options are the same as for the - # +attributes+ method. The default is to dasherize all column names, but you - # can disable this setting <tt>:dasherize</tt> to +false+. Setting <tt>:camelize</tt> - # to +true+ will camelize all column names - this also overrides <tt>:dasherize</tt>. - # To not have the column type included in the XML output set <tt>:skip_types</tt> to +true+. - # - # For instance: - # - # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ]) - # - # <topic> - # <title>The First Topic</title> - # <author-name>David</author-name> - # <approved type="boolean">false</approved> - # <content>Have a nice day</content> - # <author-email-address>david@loudthinking.com</author-email-address> - # <parent-id></parent-id> - # <last-read type="date">2004-04-15</last-read> - # </topic> - # - # To include first level associations use <tt>:include</tt>: - # - # firm.to_xml include: [ :account, :clients ] - # - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # Additionally, the record being serialized will be passed to a Proc's second - # parameter. This allows for ad hoc additions to the resultant document that - # incorporate the context of the record being serialized. And by leveraging the - # closure created by a Proc, to_xml can be used to add elements that normally fall - # outside of the scope of the model -- for example, generating and appending URLs - # associated with models. - # - # proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <name-reverse>slangis73</name-reverse> - # </firm> - # - # To include deeper levels of associations pass a hash like this: - # - # firm.to_xml include: {account: {}, clients: {include: :address}} - # <?xml version="1.0" encoding="UTF-8"?> - # <firm> - # <id type="integer">1</id> - # <rating type="integer">1</rating> - # <name>37signals</name> - # <clients type="array"> - # <client> - # <rating type="integer">1</rating> - # <name>Summit</name> - # <address> - # ... - # </address> - # </client> - # <client> - # <rating type="integer">1</rating> - # <name>Microsoft</name> - # <address> - # ... - # </address> - # </client> - # </clients> - # <account> - # <id type="integer">1</id> - # <credit-limit type="integer">50</credit-limit> - # </account> - # </firm> - # - # To include any methods on the model being called use <tt>:methods</tt>: - # - # firm.to_xml methods: [ :calculated_earnings, :real_earnings ] - # - # <firm> - # # ... normal attributes as shown above ... - # <calculated-earnings>100000000000000000</calculated-earnings> - # <real-earnings>5</real-earnings> - # </firm> - # - # To call any additional Procs use <tt>:procs</tt>. The Procs are passed a - # modified version of the options hash that was given to +to_xml+: - # - # proc = Proc.new { |options| options[:builder].tag!('abc', 'def') } - # firm.to_xml procs: [ proc ] - # - # <firm> - # # ... normal attributes as shown above ... - # <abc>def</abc> - # </firm> - # - # Alternatively, you can yield the builder object as part of the +to_xml+ call: - # - # firm.to_xml do |xml| - # xml.creator do - # xml.first_name "David" - # xml.last_name "Heinemeier Hansson" - # end - # end - # - # <firm> - # # ... normal attributes as shown above ... - # <creator> - # <first_name>David</first_name> - # <last_name>Heinemeier Hansson</last_name> - # </creator> - # </firm> - # - # As noted above, you may override +to_xml+ in your ActiveRecord::Base - # subclasses to have complete control about what's generated. The general - # form of doing this is: - # - # class IHaveMyOwnXML < ActiveRecord::Base - # def to_xml(options = {}) - # require 'builder' - # options[:indent] ||= 2 - # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent]) - # xml.instruct! unless options[:skip_instruct] - # xml.level_one do - # xml.tag!(:second_level, 'content') - # end - # end - # end - def to_xml(options = {}, &block) - XmlSerializer.new(self, options).serialize(&block) - end - end - - class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: - class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: - def compute_type - klass = @serializable.class - cast_type = klass.type_for_attribute(name) - - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || cast_type.type - - { :text => :string, - :time => :datetime }[type] || type - end - protected :compute_type - end - end -end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index d7da95c8a9..55f839444b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,5 +1,3 @@ -require 'shellwords' - module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: @@ -55,19 +53,22 @@ module ActiveRecord when String ActiveRecord::Base.dump_schemas end + + args = ['-i', '-s', '-x', '-O', '-f', filename] unless search_path.blank? - search_path = search_path.split(",").map{|search_path_part| "--schema=#{Shellwords.escape(search_path_part.strip)}" }.join(" ") + args << search_path.split(',').map do |part| + "--schema=#{part.strip}" + end.join(' ') end - - command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" - raise 'Error dumping database' unless Kernel.system(command) - + args << configuration['database'] + run_cmd('pg_dump', args, 'dumping') File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) set_psql_env - Kernel.system("psql -X -q -f #{Shellwords.escape(filename)} #{configuration['database']}") + args = [ '-q', '-f', filename, configuration['database'] ] + run_cmd('psql', args, 'loading' ) end private @@ -93,6 +94,17 @@ module ActiveRecord ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end + + def run_cmd(cmd, args, action) + fail run_cmd_error(cmd, args, action) unless Kernel.system(cmd, *args) + end + + def run_cmd_error(cmd, args, action) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 2c0cda69d0..53f3b53bec 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -20,6 +20,8 @@ require 'active_record/type/adapter_specific_registry' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' +require 'active_record/type/internal/abstract_json' + module ActiveRecord module Type @registry = AdapterSpecificRegistry.new diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb new file mode 100644 index 0000000000..963a8245d0 --- /dev/null +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -0,0 +1,33 @@ +module ActiveRecord + module Type + module Internal # :nodoc: + class AbstractJson < Type::Value # :nodoc: + include Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) rescue nil + else + value + end + end + + def serialize(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + value + end + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end + end +end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 32d17a1392..5706bbd903 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -18,7 +18,11 @@ module ActiveRecord relation = build_relation(finder_class, table, attribute, value) if record.persisted? && finder_class.primary_key.to_s != attribute.to_s - relation = relation.where.not(finder_class.primary_key => record.id) + if finder_class.primary_key + relation = relation.where.not(finder_class.primary_key => record.id) + else + raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") + end end relation = scope_relation(record, table, relation) relation = relation.merge(options[:conditions]) if options[:conditions] diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index 426b088e2f..2024aa36ab 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -24,6 +24,6 @@ class MysqlQuotingTest < ActiveRecord::MysqlTestCase @conn.stubs(:full_version).returns('5.6.3') @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) t = Time.now.change(usec: 1) - refute_match(/\.000001\z/, @conn.quoted_date(t)) + assert_no_match(/\.000001\z/, @conn.quoted_date(t)) end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb new file mode 100644 index 0000000000..c8c933af5e --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -0,0 +1,172 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_json? +class Mysql2JSONTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + class JsonDataType < ActiveRecord::Base + self.table_name = 'json_data_type' + + store_accessor :settings, :resolution + end + + def setup + @connection = ActiveRecord::Base.connection + begin + @connection.create_table('json_data_type') do |t| + t.json 'payload' + t.json 'settings' + end + end + end + + def teardown + @connection.drop_table :json_data_type, if_exists: true + JsonDataType.reset_column_information + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal :json, column.type + assert_equal 'json', column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table('json_data_type') do |t| + t.json 'users' + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash['users'] + assert_equal :json, column.type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.json\s+"settings"/, output) + end + + def test_cast_value_on_write + x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} + assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) + assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x.save + assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + data = "{\"a_key\":\"a_value\"}" + hash = type.deserialize(data) + assert_equal({'a_key' => 'a_value'}, hash) + assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) + assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + x.payload = { '"a\'' => 'b' } + assert x.save! + end + + def test_select + @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" + x = JsonDataType.first + assert_equal({'k' => 'v'}, x.payload) + end + + def test_select_multikey + @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| + x = JsonDataType.first + assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) + end + + def test_null_json + @connection.execute %q|insert into json_data_type (payload) VALUES(null)| + x = JsonDataType.first + assert_equal(nil, x.payload) + end + + def test_select_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + assert_equal(['v0', {'k1' => 'v1'}], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| + x = JsonDataType.first + x.payload = ['v1', {'k2' => 'v2'}, 'v3'] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { 'one' => 'two' } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload['three'] = 'four' + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end + + def test_assigning_invalid_json + json = JsonDataType.new + + json.payload = 'foo' + + assert_nil json.payload + end +end +end diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb index 92221e61bf..2de7e1b526 100644 --- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/quoting_test.rb @@ -16,6 +16,6 @@ class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase @connection.stubs(:full_version).returns('5.6.3') @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) t = Time.now.change(usec: 1) - refute_match(/\.000001\z/, @connection.quoted_date(t)) + assert_no_match(/\.000001\z/, @connection.quoted_date(t)) end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 73cde05504..0d2e2264e3 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -21,6 +21,9 @@ require 'models/column' require 'models/record' require 'models/admin' require 'models/admin/user' +require 'models/ship' +require 'models/treasure' +require 'models/parrot' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -91,7 +94,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -108,7 +111,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -347,6 +350,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size end + def test_belongs_to_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless') + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = Treasure.new(name: 'Gold', ship: ship) + treasure.save + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = ship.treasures.first + treasure.destroy + end + end + def test_belongs_to_counter debate = Topic.create("title" => "debate") assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index ffbf60e390..8ed7b4e39f 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1325,6 +1325,14 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preload with invalid argument" do + exception = assert_raises(ArgumentError) do + Author.preload(10).to_a + end + assert_equal('10 was not recognized for preload', exception.message) + end + + test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! 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 abbe37ab38..7718b29125 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 @@ -3,6 +3,7 @@ require 'models/developer' require 'models/computer' require 'models/project' require 'models/company' +require 'models/course' require 'models/customer' require 'models/order' require 'models/categorization' @@ -14,6 +15,7 @@ require 'models/tagging' require 'models/parrot' require 'models/person' require 'models/pirate' +require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' @@ -923,4 +925,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_deprecated { developer.projects(true) } end + + def test_alternate_database + professor = Professor.create(name: "Plum") + course = Course.create(name: "Forensics") + assert_equal 0, professor.courses.count + assert_nothing_raised do + professor.courses << course + end + assert_equal 1, professor.courses.count + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 21e2ee3b11..f487065d9d 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/ship_part' +require 'models/treasure' +require 'models/parrot' require 'models/tyre' require 'models/subscriber' require 'models/subscription' @@ -932,6 +934,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, new_firm.clients_of_firm.size end + def test_has_many_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless', treasures_count: 10) + + assert_not ship.treasures.instance_variable_get('@association').send(:has_cached_counter?) + + # Count should come from sql count() of treasures rather than treasures_count attribute + assert_equal ship.treasures.size, 0 + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.create(name: 'Gold') + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.destroy_all + end + end + def test_deleting_updates_counter_cache topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count @@ -2308,4 +2329,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_instance_of PostWithErrorDestroying, error.record end + + def test_ids_reader_memoization + car = Car.create!(name: 'Tofaş') + bulb = Bulb.create!(car: car) + + assert_equal [bulb.id], car.bulb_ids + assert_no_queries { car.bulb_ids } + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index ce2557339e..b22ce42696 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1171,4 +1171,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_deprecated { post.people(true) } end + + def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + member = Member.create! + club = Club.create! + TenantMembership.create!( + member: member, + club: club + ) + + TenantMembership.current_member = member + + tenant_clubs = member.tenant_clubs + assert_equal [club], tenant_clubs + + TenantMembership.current_member = nil + + other_member = Member.create! + other_club = Club.create! + TenantMembership.create!( + member: other_member, + club: other_club + ) + + tenant_clubs = other_member.tenant_clubs + assert_equal [other_club], tenant_clubs + ensure + TenantMembership.current_member = nil + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 2c8feb9152..b2b46812b9 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -16,6 +16,10 @@ require 'models/owner' require 'models/post' require 'models/comment' require 'models/categorization' +require 'models/customer' +require 'models/carrier' +require 'models/shop_account' +require 'models/customer_carrier' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -347,16 +351,33 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end - def test_association_force_reload_with_only_true_is_deprecated - member = Member.find(1) + def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + customer = Customer.create! + carrier = Carrier.create! + customer_carrier = CustomerCarrier.create!( + customer: customer, + carrier: carrier, + ) + account = ShopAccount.create!(customer_carrier: customer_carrier) - assert_deprecated { member.club(true) } - end + CustomerCarrier.current_customer = customer - def test_has_one_through_associations_are_mutable_unless_through_belongs_to - member_detail = MemberDetail.new(member: @member) - assert_raise(ActiveRecord::HasOneThroughCantAssociateThroughHasOneOrManyReflection) do - member_detail.membership = Membership.new - end + account_carrier = account.carrier + assert_equal carrier, account_carrier + + CustomerCarrier.current_customer = nil + + other_carrier = Carrier.create! + other_customer = Customer.create! + other_customer_carrier = CustomerCarrier.create!( + customer: other_customer, + carrier: other_carrier, + ) + other_account = ShopAccount.create!(customer_carrier: other_customer_carrier) + + account_carrier = other_account.carrier + assert_equal other_carrier, account_carrier + ensure + CustomerCarrier.current_customer = nil end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index bd74821d87..264b275181 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -152,7 +152,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( Type::Integer.new) - refute_equal string_array, int_array + assert_not_equal string_array, int_array assert_equal string_array, klass.type_for_attribute("my_array") assert_equal int_array, klass.type_for_attribute("my_int_array") end @@ -167,7 +167,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( Type::Integer.new) - refute_equal string_range, int_range + assert_not_equal string_range, int_range assert_equal string_range, klass.type_for_attribute("my_range") assert_equal int_range, klass.type_for_attribute("my_int_range") end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 0791dde1f2..48dc8135ab 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_raise_if_select_is_set_without_id - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Post.select(:title).find_each(batch_size: 1) { |post| flunk "should not call this block" } @@ -199,7 +199,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_return_an_enumerator enum = nil - assert_queries(0) do + assert_no_queries do enum = Post.find_in_batches(:batch_size => 1) end assert_queries(4) do @@ -210,6 +210,234 @@ class EachTest < ActiveRecord::TestCase end end + def test_in_batches_should_not_execute_any_query + assert_no_queries do + assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2) + end + end + + def test_in_batches_should_yield_relation_if_block_given + assert_queries(6) do + Post.in_batches(of: 2) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_be_enumerable_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_each_record_should_yield_record_if_block_is_given + assert_queries(6) do + Post.in_batches(of: 2).each_record do |post| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_return_enumerator_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_be_ordered_by_id + ids = Post.order('id ASC').pluck(:id) + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert_equal ids[i], post.id + end + end + end + + def test_in_batches_update_all_affect_all_records + assert_queries(6 + 6) do # 6 selects, 6 updates + Post.in_batches(of: 2).update_all(title: "updated-title") + end + assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count + end + + def test_in_batches_delete_all_should_not_delete_records_in_other_batches + not_deleted_count = Post.where('id <= 2').count + Post.where('id > 2').in_batches(of: 2).delete_all + assert_equal 0, Post.where('id > 2').count + assert_equal not_deleted_count, Post.count + end + + def test_in_batches_should_not_be_loaded + Post.in_batches(of: 1) do |relation| + assert_not relation.loaded? + end + + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + + def test_in_batches_should_be_loaded + Post.in_batches(of: 1, load: true) do |relation| + assert relation.loaded? + end + end + + def test_in_batches_if_not_loaded_executes_more_queries + assert_queries(@total + 1) do + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + end + + def test_in_batches_should_return_relations + assert_queries(@total + 1) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_start_from_the_start_option + post = Post.order('id ASC').where('id >= ?', 2).first + assert_queries(2) do + relation = Post.in_batches(of: 1, begin_at: 2).first + assert_equal post, relation.first + end + end + + def test_in_batches_should_end_at_the_end_option + post = Post.order('id DESC').where('id <= ?', 5).first + assert_queries(7) do + relation = Post.in_batches(of: 1, end_at: 5, load: true).reverse_each.first + assert_equal post, relation.last + end + end + + def test_in_batches_shouldnt_execute_query_unless_needed + assert_queries(2) do + Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + + assert_queries(1) do + Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + end + + def test_in_batches_should_quote_batch_order + c = Post.connection + assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified + not_a_post = "not a post" + def not_a_post.id + raise StandardError.new("not_a_post had #id called on it") + end + + assert_nothing_raised do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + + relation = [not_a_post] * relation.count + end + end + end + + def test_in_batches_should_not_ignore_default_scope_without_order_statements + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.in_batches do |relation| + posts.concat(relation) + end + assert_equal special_posts_ids, posts.map(&:id) + end + + def test_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.in_batches({ of: 42, begin_at: 1 }.freeze){} + end + end + + def test_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.in_batches(of: 1, begin_at: start_nick) do |relation| + subscribers.concat(relation) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + assert_queries(Subscriber.count + 1) do + Subscriber.in_batches(of: 1, load: true) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Subscriber, relation.first + end + end + end + + def test_in_batches_should_return_an_enumerator + enum = nil + assert_no_queries do + enum = Post.in_batches(of: 1) + end + assert_queries(4) do + enum.first(4) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_relations_should_not_overlap_with_each_other + seen_posts = [] + Post.in_batches(of: 2, load: true) do |relation| + relation.to_a.each do |post| + assert_not seen_posts.include?(post) + seen_posts << post + end + end + end + + def test_in_batches_relations_with_condition_should_not_overlap_with_each_other + seen_posts = [] + author_id = Post.first.author_id + posts_by_author = Post.where(author_id: author_id) + Post.in_batches(of: 2) do |batch| + seen_posts += batch.where(author_id: author_id) + end + + assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort + end + + def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches + Post.update_all(author_id: 0) + person = Post.last + person.update_attributes(author_id: 1) + + Post.in_batches(of: 2) do |batch| + batch.where('author_id >= 1').update_all('author_id = author_id + 1') + end + assert_equal 2, person.reload.author_id # incremented only once + end + def test_find_in_batches_start_deprecated assert_deprecated do assert_queries(@total) do diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb new file mode 100644 index 0000000000..724234d7f4 --- /dev/null +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -0,0 +1,70 @@ +require "cases/helper" +require "models/computer" +require "models/developer" +require "models/project" +require "models/topic" +require "models/post" +require "models/comment" + +module ActiveRecord + class CollectionCacheKeyTest < ActiveRecord::TestCase + fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts + + test "collection_cache_key on model" do + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + end + + test "cache_key for relation" do + developers = Developer.where(name: "David") + last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "it triggers at most one query" do + developers = Developer.where(name: "David") + + assert_queries(1) { developers.cache_key } + assert_queries(0) { developers.cache_key } + end + + test "it doesn't trigger any query if the relation is already loaded" do + developers = Developer.where(name: "David").load + assert_queries(0) { developers.cache_key } + end + + test "relation cache_key changes when the sql query changes" do + developers = Developer.where(name: "David") + other_relation = Developer.where(name: "David").where("1 = 1") + + assert_not_equal developers.cache_key, other_relation.cache_key + end + + test "cache_key for empty relation" do + developers = Developer.where(name: "Non Existent Developer") + assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + end + + test "cache_key with custom timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:nsec) + assert_match(last_topic_timestamp, topics.cache_key(:written_on)) + end + + test "cache_key with unknown timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) } + end + + test "collection proxy provides a cache_key" do + developers = projects(:active_record).developers + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + end + end +end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index ba23049a92..7c930de97b 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -40,43 +40,43 @@ class EnumTest < ActiveRecord::TestCase published, written = Book.statuses[:published], Book.statuses[:written] assert_equal @book, Book.where(status: published).first - refute_equal @book, Book.where(status: written).first + assert_not_equal @book, Book.where(status: written).first assert_equal @book, Book.where(status: [published]).first - refute_equal @book, Book.where(status: [written]).first - refute_equal @book, Book.where("status <> ?", published).first + assert_not_equal @book, Book.where(status: [written]).first + assert_not_equal @book, Book.where("status <> ?", published).first assert_equal @book, Book.where("status <> ?", written).first end test "find via where with symbols" do assert_equal @book, Book.where(status: :published).first - refute_equal @book, Book.where(status: :written).first + assert_not_equal @book, Book.where(status: :written).first assert_equal @book, Book.where(status: [:published]).first - refute_equal @book, Book.where(status: [:written]).first - refute_equal @book, Book.where.not(status: :published).first + assert_not_equal @book, Book.where(status: [:written]).first + assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end test "find via where with strings" do assert_equal @book, Book.where(status: "published").first - refute_equal @book, Book.where(status: "written").first + assert_not_equal @book, Book.where(status: "written").first assert_equal @book, Book.where(status: ["published"]).first - refute_equal @book, Book.where(status: ["written"]).first - refute_equal @book, Book.where.not(status: "published").first + assert_not_equal @book, Book.where(status: ["written"]).first + assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first end test "build from scope" do assert Book.written.build.written? - refute Book.written.build.proposed? + assert_not Book.written.build.proposed? end test "build from where" do assert Book.where(status: Book.statuses[:written]).build.written? - refute Book.where(status: Book.statuses[:written]).build.proposed? + assert_not Book.where(status: Book.statuses[:written]).build.proposed? assert Book.where(status: :written).build.written? - refute Book.where(status: :written).build.proposed? + assert_not Book.where(status: :written).build.proposed? assert Book.where(status: "written").build.written? - refute Book.where(status: "written").build.proposed? + assert_not Book.where(status: "written").build.proposed? end test "update by declaration" do diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 03a187ae92..76ea950fb1 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -217,6 +217,13 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_yaml_file_with_invalid_column + e = assert_raise(ActiveRecord::Fixture::FixtureError) do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") + end + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb new file mode 100644 index 0000000000..e4772905bb --- /dev/null +++ b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb @@ -0,0 +1,93 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + if current_adapter?(:PostgreSQLAdapter) + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + end + + private + def assert_column_exists(column_name) + columns = connection.columns(table_name) + assert columns.map(&:name).include?(column_name.to_s) + end + + def assert_type_correct(column_name, type) + columns = connection.columns(table_name) + column = columns.select{ |c| c.name == column_name.to_s }.first + assert_equal type.to_s, column.sql_type + end + + end + end +end
\ No newline at end of file diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b2f209fe97..11e42f28dc 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -132,13 +132,9 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :testings2, if_exists: true end - def connection - ActiveRecord::Base.connection - end - def test_migration_instance_has_connection migration = Class.new(ActiveRecord::Migration).new - assert_equal connection, migration.connection + assert_equal ActiveRecord::Base.connection, migration.connection end def test_method_missing_delegates_to_connection diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0dbc60940e..86316ab476 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/developer' require 'models/computer' +require 'models/vehicle' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -453,4 +454,9 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, scope.where_clause.ast.children.length assert_equal Developer.where(name: "David"), scope end + + def test_with_abstract_class_where_clause_should_not_be_duplicated + scope = Bus.all + assert_equal scope.where_clause.ast.children.length, 1 + end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 084302cde5..184ff7fc63 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -204,7 +204,7 @@ module ActiveRecord end def test_structure_dump - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -212,7 +212,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -220,7 +220,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) with_dump_schemas(:all) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -228,7 +228,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-i', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -261,14 +261,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -X -q -f #{filename} my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -X -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 49ada22529..b3c42c8e42 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -11,7 +11,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_laster_raise_if_non_persisted invoice = Invoice.new Invoice.transaction do - refute invoice.persisted? + assert_not invoice.persisted? assert_raises(ActiveRecord::ActiveRecordError) do invoice.touch_later end @@ -21,7 +21,7 @@ class TouchLaterTest < ActiveRecord::TestCase def test_touch_later_dont_set_dirty_attributes invoice = Invoice.create! invoice.touch_later - refute invoice.changed? + assert_not invoice.changed? end def test_touch_later_update_the_attributes diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index ceca2c8366..7502a55391 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -4,6 +4,7 @@ require 'models/reply' require 'models/warehouse_thing' require 'models/guid' require 'models/event' +require 'models/dashboard' class Wizard < ActiveRecord::Base self.abstract_class = true @@ -446,4 +447,26 @@ class UniquenessValidationTest < ActiveRecord::TestCase key2.key_number = 10 assert_not key2.valid? end + + def test_validate_uniqueness_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + + validates_uniqueness_of :dashboard_id + + def self.name; "Dashboard" end + end + + abc = klass.create!(dashboard_id: "abc") + assert klass.new(dashboard_id: "xyz").valid? + assert_not klass.new(dashboard_id: "abc").valid? + + abc.dashboard_id = "def" + + e = assert_raises ActiveRecord::UnknownPrimaryKey do + abc.save! + end + assert_match(/\AUnknown primary key for table dashboards in model/, e.message) + assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) + end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml new file mode 100644 index 0000000000..3e10331105 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -0,0 +1,2 @@ +george: + arrr: "Curious George" diff --git a/activerecord/test/models/carrier.rb b/activerecord/test/models/carrier.rb new file mode 100644 index 0000000000..230be118c3 --- /dev/null +++ b/activerecord/test/models/carrier.rb @@ -0,0 +1,2 @@ +class Carrier < ActiveRecord::Base +end diff --git a/activerecord/test/models/categorization.rb b/activerecord/test/models/categorization.rb index 6588531de6..4cd67c970a 100644 --- a/activerecord/test/models/categorization.rb +++ b/activerecord/test/models/categorization.rb @@ -1,6 +1,6 @@ class Categorization < ActiveRecord::Base belongs_to :post - belongs_to :category + belongs_to :category, counter_cache: true belongs_to :named_category, :class_name => 'Category', :foreign_key => :named_category_name, :primary_key => :name belongs_to :author diff --git a/activerecord/test/models/customer_carrier.rb b/activerecord/test/models/customer_carrier.rb new file mode 100644 index 0000000000..37186903ff --- /dev/null +++ b/activerecord/test/models/customer_carrier.rb @@ -0,0 +1,14 @@ +class CustomerCarrier < ActiveRecord::Base + cattr_accessor :current_customer + + belongs_to :customer + belongs_to :carrier + + default_scope -> { + if current_customer + where(customer: current_customer) + else + all + end + } +end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index dc0566d8a7..7693c6e515 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -26,6 +26,9 @@ class Member < ActiveRecord::Base has_many :current_memberships, -> { where :favourite => true } has_many :clubs, :through => :current_memberships + has_many :tenant_memberships + has_many :tenant_clubs, through: :tenant_memberships, class_name: 'Club', source: :club + has_one :club_through_many, :through => :current_memberships, :source => :club belongs_to :admittable, polymorphic: true diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index df7167ee93..e181ba1f11 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -18,3 +18,18 @@ class SelectedMembership < Membership select("'1' as foo") end end + +class TenantMembership < Membership + cattr_accessor :current_member + + belongs_to :member + belongs_to :club + + default_scope -> { + if current_member + where(member: current_member) + else + all + end + } +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 10f13b67da..81a18188d4 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -98,11 +98,11 @@ class Post < ActiveRecord::Base end end - has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all - has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy + has_many :taggings_with_delete_all, :class_name => 'Tagging', :as => :taggable, :dependent => :delete_all, counter_cache: :taggings_with_delete_all_count + has_many :taggings_with_destroy, :class_name => 'Tagging', :as => :taggable, :dependent => :destroy, counter_cache: :taggings_with_destroy_count - has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy - has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify + has_many :tags_with_destroy, :through => :taggings, :source => :tag, :dependent => :destroy, counter_cache: :tags_with_destroy_count + has_many :tags_with_nullify, :through => :taggings, :source => :tag, :dependent => :nullify, counter_cache: :tags_with_nullify_count has_many :misc_tags, -> { where :tags => { :name => 'Misc' } }, :through => :taggings, :source => :tag has_many :funky_tags, :through => :taggings, :source => :tag diff --git a/activerecord/test/models/professor.rb b/activerecord/test/models/professor.rb new file mode 100644 index 0000000000..7654eda0ef --- /dev/null +++ b/activerecord/test/models/professor.rb @@ -0,0 +1,5 @@ +require_dependency 'models/arunit2_model' + +class Professor < ARUnit2Model + has_and_belongs_to_many :courses +end diff --git a/activerecord/test/models/shop_account.rb b/activerecord/test/models/shop_account.rb new file mode 100644 index 0000000000..1580e8b20c --- /dev/null +++ b/activerecord/test/models/shop_account.rb @@ -0,0 +1,6 @@ +class ShopAccount < ActiveRecord::Base + belongs_to :customer + belongs_to :customer_carrier + + has_one :carrier, through: :customer_carrier +end diff --git a/activerecord/test/models/treasure.rb b/activerecord/test/models/treasure.rb index ffc65466d5..63ff0c23ec 100644 --- a/activerecord/test/models/treasure.rb +++ b/activerecord/test/models/treasure.rb @@ -1,6 +1,7 @@ class Treasure < ActiveRecord::Base has_and_belongs_to_many :parrots belongs_to :looter, :polymorphic => true + # No counter_cache option given belongs_to :ship has_many :price_estimates, :as => :estimate_of diff --git a/activerecord/test/models/vehicle.rb b/activerecord/test/models/vehicle.rb new file mode 100644 index 0000000000..ef26170f1f --- /dev/null +++ b/activerecord/test/models/vehicle.rb @@ -0,0 +1,7 @@ +class Vehicle < ActiveRecord::Base + self.abstract_class = true + default_scope -> { where("tires_count IS NOT NULL") } +end + +class Bus < Vehicle +end
\ No newline at end of file diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7bab675b2a..6f34115534 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -131,6 +131,8 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :carriers, force: true + create_table :categories, force: true do |t| t.string :name, null: false t.string :type @@ -237,6 +239,11 @@ ActiveRecord::Schema.define do t.string :gps_location end + create_table :customer_carriers, force: true do |t| + t.references :customer + t.references :carrier + end + create_table :dashboards, force: true, id: false do |t| t.string :dashboard_id t.string :name @@ -667,6 +674,8 @@ ActiveRecord::Schema.define do t.string :name t.integer :pirate_id t.integer :update_only_pirate_id + # Conventionally named column for counter_cache + t.integer :treasures_count, default: 0 t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -683,6 +692,11 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :shop_accounts, force: true do |t| + t.references :customer + t.references :customer_carrier + end + create_table :speedometers, force: true, id: false do |t| t.string :speedometer_id t.string :name @@ -939,3 +953,12 @@ end College.connection.create_table :colleges, force: true do |t| t.column :name, :string, null: false end + +Professor.connection.create_table :professors, force: true do |t| + t.column :name, :string, null: false +end + +Professor.connection.create_table :courses_professors, id: false, force: true do |t| + t.references :course + t.references :professor +end diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index d11fd9cfc1..c5334e8596 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -1,6 +1,7 @@ require 'active_support/logger' require 'models/college' require 'models/course' +require 'models/professor' module ARTest def self.connection_name |