diff options
Diffstat (limited to 'activerecord')
31 files changed, 520 insertions, 325 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a45d912017..9138230b5e 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,50 @@ +* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`. + + If a `has_many` association is adjusted using a scope, and another `has_many :through` + uses this association, then the scope adjustment is unexpectedly neglected. + + Fixes #14537. + + *Jan Habermann* + +* `@destroyed` should always be set to `false` when an object is duped. + + *Kuldeep Aggarwal* + +* Fixed has_many association to make it support irregular inflections. + + Fixes #8928. + + *arthurnn*, *Javier Goizueta* + +* Fixed a problem where count used with a grouping was not returning a Hash. + + Fixes #14721. + + *Eric Chahin* + +* `sanitize_sql_like` helper method to escape a string for safe use in a SQL + LIKE statement. + + Example: + + class Article + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term)) + end + end + + Article.search("20% _reduction_") + # => Query looks like "... title LIKE '20\% \_reduction\_' ..." + + *Rob Gilson*, *Yves Senn* + +* Do not quote uuid default value on `change_column`. + + Fixes #14604. + + *Eric Chahin* + * The comparison between `Relation` and `CollectionProxy` should be consistent. Example: diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 45f1b07f69..5a84792f45 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -9,14 +9,6 @@ module ActiveRecord @association end - def size - @association.size - end - - def empty? - @association.empty? - end - def ==(other) other == to_a end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 8272a5584c..1edd4fa3aa 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -31,6 +31,14 @@ module ActiveRecord @updated end + def decrement_counters # :nodoc: + with_cache_name { |name| decrement_counter name } + end + + def increment_counters # :nodoc: + with_cache_name { |name| increment_counter name } + end + private def find_target? @@ -51,13 +59,15 @@ module ActiveRecord end end - def decrement_counters - with_cache_name { |name| decrement_counter name } + def decrement_counter(counter_cache_name) + if foreign_key_present? + klass.decrement_counter(counter_cache_name, target_id) + end end - def decrement_counter counter_cache_name + def increment_counter(counter_cache_name) if foreign_key_present? - klass.decrement_counter(counter_cache_name, target_id) + klass.increment_counter(counter_cache_name, target_id) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 11be92ae01..47cc1f4b34 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -26,29 +26,9 @@ module ActiveRecord::Associations::Builder private def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_create + return if mixin.method_defined? :belongs_to_counter_cache_after_update mixin.class_eval do - def belongs_to_counter_cache_after_create(reflection) - if record = send(reflection.name) - cache_column = reflection.counter_cache_column - record.class.increment_counter(cache_column, record.id) - @_after_create_counter_called = true - end - end - - def belongs_to_counter_cache_after_destroy(reflection) - foreign_key = reflection.foreign_key.to_sym - unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - record = send reflection.name - if record && self.actually_destroyed? - cache_column = reflection.counter_cache_column - record.class.decrement_counter(cache_column, record.id) - self.clear_destroy_state - end - end - end - def belongs_to_counter_cache_after_update(reflection) foreign_key = reflection.foreign_key cache_column = reflection.counter_cache_column @@ -74,14 +54,6 @@ module ActiveRecord::Associations::Builder def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column - model.after_create lambda { |record| - record.belongs_to_counter_cache_after_create(reflection) - } - - model.after_destroy lambda { |record| - record.belongs_to_counter_cache_after_destroy(reflection) - } - model.after_update lambda { |record| record.belongs_to_counter_cache_after_update(reflection) } diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index ba7d2a3782..f8a85b8a6f 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -14,9 +14,11 @@ module ActiveRecord def target_scope scope = super chain.drop(1).each do |reflection| + relation = reflection.klass.all + relation.merge!(reflection.scope) if reflection.scope + scope.merge!( - reflection.klass.all. - except(:select, :create_with, :includes, :preload, :joins, :eager_load) + relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end scope diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 20eea208ec..d2ef83b047 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -187,7 +187,6 @@ module ActiveRecord include Arel::Visitors::BindVisitor end - # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) super(connection, logger) @connection_options, @config = connection_options, config diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index a8ab52be74..b79d1a4458 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -62,8 +62,8 @@ module ActiveRecord # Converts the query parameters of the URI into a hash. # - # "localhost?pool=5&reap_frequency=2" - # # => { "pool" => "5", "reap_frequency" => "2" } + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } # # returns empty hash if no query present. # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index a5fb048b1e..168b08ba75 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -44,10 +44,32 @@ module ActiveRecord end end + def select_value(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 + end + end + + def select_values(arel, name = nil) + arel, binds = binds_from_relation arel, [] + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + if result.nfields > 0 + result.column_values(0) + else + [] + end + end + end + # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + execute_and_clear(sql, name, binds) do |result| + result.values + end end # Executes an INSERT query and returns the new record's ID @@ -134,28 +156,20 @@ module ActiveRecord end def exec_query(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - - types = {} - fields = result.fields - fields.each_with_index do |fname, i| - ftype = result.ftype i - fmod = result.fmod i - types[fname] = get_oid_type(ftype, fmod, fname) + execute_and_clear(sql, name, binds) do |result| + types = {} + fields = result.fields + fields.each_with_index do |fname, i| + ftype = result.ftype i + fmod = result.fmod i + types[fname] = get_oid_type(ftype, fmod, fname) + end + ActiveRecord::Result.new(fields, result.values, types) end - - ret = ActiveRecord::Result.new(fields, result.values, types) - result.clear - return ret end def exec_delete(sql, name = 'SQL', binds = []) - result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : - exec_cache(sql, name, binds) - affected = result.cmd_tuples - result.clear - affected + execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } end alias :exec_update :exec_delete diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index ac3b0f713d..403e37fde9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -182,6 +182,15 @@ module ActiveRecord end result end + + # Does not quote function default values for UUID columns + def quote_default_value(value, column) #:nodoc: + if column.type == :uuid && value =~ /\(\)/ + value + else + quote(value) + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 1229b4851a..1dc7a6f0fd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -187,6 +187,10 @@ module ActiveRecord end end + def column_for(table_name, column_name) #:nodoc: + columns(table_name).detect { |c| c.name == column_name.to_s } + end + # Returns the current database name. def current_database query('select current_database()', 'SCHEMA')[0][0] @@ -404,13 +408,15 @@ module ActiveRecord # Changes the default value of a table column. def change_column_default(table_name, column_name, default) clear_cache! - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}" + column = column_for(table_name, column_name) + execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column end def change_column_null(table_name, column_name, null, default = nil) clear_cache! unless null || default.nil? - execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") + column = column_for(table_name, column_name) + execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column end execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 0485093123..1f1bd342d1 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -662,6 +662,14 @@ module ActiveRecord FEATURE_NOT_SUPPORTED = "0A000" #:nodoc: + def execute_and_clear(sql, name, binds) + result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) : + exec_cache(sql, name, binds) + ret = yield result + result.clear + ret + end + def exec_no_cache(sql, name, binds) log(sql, name, binds) { @connection.async_exec(sql) } end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index cd1f7a16c6..2d5c47967d 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -30,7 +30,7 @@ module ActiveRecord db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout] - ConnectionAdapters::SQLite3Adapter.new(db, logger, config) + ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config) rescue Errno::ENOENT => error if error.message.include?("No such file or directory") raise ActiveRecord::NoDatabaseError.new(error.message, error) @@ -127,7 +127,7 @@ module ActiveRecord include Arel::Visitors::BindVisitor end - def initialize(connection, logger, config) + def initialize(connection, logger, connection_options, config) super(connection, logger) @active = nil diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 4e53f66005..d6df98a80f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -267,6 +267,7 @@ module ActiveRecord @attributes_cache = {} @new_record = true + @destroyed = false super end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index a5897edf03..b7b790322a 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -131,13 +131,41 @@ module ActiveRecord private + def _create_record(*) + id = super + + each_counter_cached_associations do |association| + if send(association.reflection.name) + association.increment_counters + @_after_create_counter_called = true + end + end + + id + end + def destroy_row affected_rows = super - @_actually_destroyed = affected_rows > 0 + if affected_rows > 0 + each_counter_cached_associations do |association| + foreign_key = association.reflection.foreign_key.to_sym + unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key + if send(association.reflection.name) + association.decrement_counters + end + end + end + end affected_rows end + def each_counter_cached_associations + reflections.each do |name, reflection| + yield association(name) if reflection.belongs_to? && reflection.counter_cache_column + end + end + end end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 5b255c3fe5..05d0c41678 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -43,7 +43,7 @@ module ActiveRecord end def count(*) - 0 + calculate :count, nil end def sum(*) @@ -54,7 +54,7 @@ module ActiveRecord # TODO: Remove _options argument as soon we remove support to # activerecord-deprecated_finders. if operation == :count - 0 + group_values.any? ? Hash.new : 0 else nil end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1724ea95b0..4fde6677be 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -449,9 +449,9 @@ module ActiveRecord end def derive_class_name - class_name = name.to_s.camelize + class_name = name.to_s class_name = class_name.singularize if collection? - class_name + class_name.camelize end def derive_foreign_key diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 812e3e800a..514ebc2bfe 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -188,7 +188,7 @@ module ActiveRecord private def has_include?(column_name) - eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?)) + eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?)) end def perform_calculation(operation, column_name, options = {}) diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 5a71c13d91..be62e41932 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -107,6 +107,13 @@ module ActiveRecord end.join(', ') end + # Sanitizes a +string+ so that it is safe to use within a sql + # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" + def sanitize_sql_like(string, escape_character = "\\") + pattern = Regexp.union(escape_character, "%", "_") + string.gsub(pattern) { |x| [escape_character, x].join } + end + # Accepts an array of conditions. The array has each value # sanitized and interpolated into the SQL statement. # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'" diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 6ce0495f6f..168b338b97 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -28,7 +28,7 @@ module ActiveRecord # Example usage of +DatabaseTasks+ outside Rails could look as such: # # include ActiveRecord::Tasks - # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml')) + # DatabaseTasks.database_configuration = YAML.load_file('my_database_config.yml') # DatabaseTasks.db_dir = 'db' # # other settings... # diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 9e03ea6bee..bdf8e15e3e 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -40,6 +40,19 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase drop_table "uuid_data_type" end + def test_change_column_default + @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" + UUIDType.reset_column_information + column = UUIDType.columns.find { |c| c.name == 'thingy' } + assert_equal "uuid_generate_v1()", column.default_function + + @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" + + UUIDType.reset_column_information + column = UUIDType.columns.find { |c| c.name == 'thingy' } + assert_equal "uuid_generate_v4()", column.default_function + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type 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 6675e19dd9..e30577fb49 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1116,5 +1116,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club.reload assert_equal [member], club.favourites + + def test_has_many_through_unscope_default_scope + post = Post.create!(:title => 'Beaches', :body => "I like beaches!") + Reader.create! :person => people(:david), :post => post + LazyReader.create! :person => people(:susan), :post => post + + assert_equal 2, post.people.to_a.size + assert_equal 1, post.lazy_people.to_a.size + + assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size + assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size end end diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb deleted file mode 100644 index deed226eab..0000000000 --- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb +++ /dev/null @@ -1,56 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - class ConnectionPool - def insert_connection_for_test!(c) - synchronize do - @connections << c - @available.add c - end - end - end - - class AbstractAdapterTest < ActiveRecord::TestCase - attr_reader :adapter - - def setup - @adapter = AbstractAdapter.new nil, nil - end - - def test_in_use? - assert_not adapter.in_use?, 'adapter is not in use' - assert adapter.lease, 'lease adapter' - assert adapter.in_use?, 'adapter is in use' - end - - def test_lease_twice - assert adapter.lease, 'should lease adapter' - assert_not adapter.lease, 'should not lease adapter' - end - - def test_expire_mutates_in_use - assert adapter.lease, 'lease adapter' - assert adapter.in_use?, 'adapter is in use' - adapter.expire - assert_not adapter.in_use?, 'adapter is in use' - end - - def test_close - pool = ConnectionPool.new(ConnectionSpecification.new({}, nil)) - pool.insert_connection_for_test! adapter - adapter.pool = pool - - # Make sure the pool marks the connection in use - assert_equal adapter, pool.connection - assert adapter.in_use? - - # Close should put the adapter back in the pool - adapter.close - assert_not adapter.in_use? - - assert_equal adapter, pool.connection - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb new file mode 100644 index 0000000000..662e19f35e --- /dev/null +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class AdapterLeasingTest < ActiveRecord::TestCase + class Pool < ConnectionPool + def insert_connection_for_test!(c) + synchronize do + @connections << c + @available.add c + end + end + end + + def setup + @adapter = AbstractAdapter.new nil, nil + end + + def test_in_use? + assert_not @adapter.in_use?, 'adapter is not in use' + assert @adapter.lease, 'lease adapter' + assert @adapter.in_use?, 'adapter is in use' + end + + def test_lease_twice + assert @adapter.lease, 'should lease adapter' + assert_not @adapter.lease, 'should not lease adapter' + end + + def test_expire_mutates_in_use + assert @adapter.lease, 'lease adapter' + assert @adapter.in_use?, 'adapter is in use' + @adapter.expire + assert_not @adapter.in_use?, 'adapter is in use' + end + + def test_close + pool = Pool.new(ConnectionSpecification.new({}, nil)) + pool.insert_connection_for_test! @adapter + @adapter.pool = pool + + # Make sure the pool marks the connection in use + assert_equal @adapter, pool.connection + assert @adapter.in_use? + + # Close should put the adapter back in the pool + @adapter.close + assert_not @adapter.in_use? + + assert_equal @adapter, pool.connection + end + end + end +end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index f2d18e812d..3e33b30144 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -2,199 +2,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters - - class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase - - def klass - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig - end - - def setup - @previous_database_url = ENV.delete("DATABASE_URL") - end - - teardown do - ENV["DATABASE_URL"] = @previous_database_url - end - - def resolve(spec, config) - ConnectionSpecification::Resolver.new(klass.new(config).resolve).resolve(spec) - end - - def spec(spec, config) - ConnectionSpecification::Resolver.new(klass.new(config).resolve).spec(spec) - end - - def test_resolver_with_database_uri_and_current_env_symbol_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = resolve(:default_env, config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_and_current_env_string_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = assert_deprecated { resolve("default_env", config) } - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_known_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } - actual = resolve(:production, config) - expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_resolver_with_database_uri_and_unknown_symbol_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - assert_raises AdapterNotSpecified do - resolve(:production, config) - end - end - - def test_resolver_with_database_uri_and_unknown_string_key - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } - assert_deprecated do - assert_raises AdapterNotSpecified do - spec("production", config) - end - end - end - - def test_resolver_with_database_uri_and_supplied_url - ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" - config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } - actual = resolve("postgres://localhost/foo", config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_jdbc_url - config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } } - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_environment_does_not_exist_in_config_url_does_exist - ENV['DATABASE_URL'] = "postgres://localhost/foo" - config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } - actual = klass.new(config).resolve - expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } - assert_equal expect_prod, actual["default_env"] - end - - def test_url_with_hyphenated_scheme - ENV['DATABASE_URL'] = "ibm-db://localhost/foo" - config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } - actual = resolve(:default_env, config) - expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } - assert_equal expected, actual - end - - def test_string_connection - config = { "default_env" => "postgres://localhost/foo" } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_url_sub_key - config = { "default_env" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_hash - config = { "production" => { "adapter" => "postgres", "database" => "foo" } } - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank - config = {} - actual = klass.new(config).resolve - assert_equal config, actual - end - - def test_blank_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {} - actual = klass.new(config).resolve - expected = { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" } - assert_equal expected, actual["default_env"] - assert_equal nil, actual["production"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] - end - - def test_url_sub_key_with_database_url - ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" - - config = { "default_env" => { "url" => "postgres://localhost/foo" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost" - } - } - assert_equal expected, actual - end - - def test_merge_no_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"default_env" => { "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - - def test_merge_conflicts_with_database_url - ENV['DATABASE_URL'] = "postgres://localhost/foo" - - config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } - actual = klass.new(config).resolve - expected = { "default_env" => - { "adapter" => "postgresql", - "database" => "foo", - "host" => "localhost", - "pool" => "5" - } - } - assert_equal expected, actual - end - end - class ConnectionHandlerTest < ActiveRecord::TestCase def setup @klass = Class.new(Base) { def self.name; 'klass'; end } diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb new file mode 100644 index 0000000000..da852aaa02 --- /dev/null +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -0,0 +1,192 @@ +require "cases/helper" + +module ActiveRecord + module ConnectionAdapters + class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase + def setup + @previous_database_url = ENV.delete("DATABASE_URL") + end + + teardown do + ENV["DATABASE_URL"] = @previous_database_url + end + + def resolve_config(config) + ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + end + + def resolve_spec(spec, config) + ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + end + + def test_resolver_with_database_uri_and_current_env_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_and_current_env_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = assert_deprecated { resolve_spec("default_env", config) } + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_known_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:production, config) + expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_unknown_symbol_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_raises AdapterNotSpecified do + resolve_spec(:production, config) + end + end + + def test_resolver_with_database_uri_and_unknown_string_key + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + assert_deprecated do + assert_raises AdapterNotSpecified do + resolve_spec("production", config) + end + end + end + + def test_resolver_with_database_uri_and_supplied_url + ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo" + config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } + actual = resolve_spec("postgres://localhost/foo", config) + expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_jdbc_url + config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_environment_does_not_exist_in_config_url_does_exist + ENV['DATABASE_URL'] = "postgres://localhost/foo" + config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_config(config) + expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + assert_equal expect_prod, actual["default_env"] + end + + def test_url_with_hyphenated_scheme + ENV['DATABASE_URL'] = "ibm-db://localhost/foo" + config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } + actual = resolve_spec(:default_env, config) + expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" } + assert_equal expected, actual + end + + def test_string_connection + config = { "default_env" => "postgres://localhost/foo" } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_url_sub_key + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_hash + config = { "production" => { "adapter" => "postgres", "database" => "foo" } } + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank + config = {} + actual = resolve_config(config) + assert_equal config, actual + end + + def test_blank_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + assert_equal expected, actual["default_env"] + assert_equal nil, actual["production"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_url_sub_key_with_database_url + ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO" + + config = { "default_env" => { "url" => "postgres://localhost/foo" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" + } + } + assert_equal expected, actual + end + + def test_merge_no_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + + def test_merge_conflicts_with_database_url + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } + actual = resolve_config(config) + expected = { "default_env" => + { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost", + "pool" => "5" + } + } + assert_equal expected, actual + end + end + end +end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 046fe83e54..9209672ac5 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -233,6 +233,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_nothing_raised { Minimalistic.create!(:id => 2) } end + def test_save_with_duping_of_destroyed_object + developer = Developer.create(name: "Kuldeep") + developer.destroy + new_developer = developer.dup + new_developer.save + assert new_developer.persisted? + end + + def test_dup_of_destroyed_object_is_not_destroyed + developer = Developer.create(name: "Kuldeep") + developer.destroy + new_developer = developer.dup + new_developer.save + assert_equal new_developer.destroyed?, false + end + def test_create_many topics = Topic.create([ { "title" => "first" }, { "title" => "second" }]) assert_equal 2, topics.size diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index fed199f6e9..c085fcf161 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -87,6 +87,14 @@ class ReflectionTest < ActiveRecord::TestCase end end + def test_irregular_reflection_class_name + ActiveSupport::Inflector.inflections do |inflect| + inflect.irregular 'plural_irregular', 'plurales_irregulares' + end + reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + assert_equal 'PluralIrregular', reflection.class_name + end + def test_aggregation_reflection reflection_for_address = AggregateReflection.new( :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 562cfe6796..d054dfa25a 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -14,6 +14,7 @@ require 'models/car' require 'models/engine' require 'models/tyre' require 'models/minivan' +require 'models/aircraft' class RelationTest < ActiveRecord::TestCase @@ -365,6 +366,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash) end + + def test_null_relation_count + ac = Aircraft.new + assert_equal Hash.new, ac.engines.group(:id).count + assert_equal 0, ac.engines.count + ac.save + assert_equal Hash.new, ac.engines.group(:id).count + assert_equal 0, ac.engines.count + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -864,6 +875,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal 9, posts.where(:comments_count => 0).count end + def test_count_on_association_relation + author = Author.last + another_author = Author.first + posts = Post.where(author_id: author.id) + + assert_equal author.posts.where(author_id: author.id).size, posts.count + + assert_equal 0, author.posts.where(author_id: another_author.id).size + assert author.posts.where(author_id: another_author.id).empty? + end + def test_count_with_distinct posts = Post.all diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 954eab8022..dca85fb5eb 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -51,4 +51,31 @@ class SanitizeTest < ActiveRecord::TestCase select_author_sql = Post.send(:sanitize_sql_array, ['']) assert_equal('', select_author_sql) end + + def test_sanitize_sql_like + assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%') + assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string') + assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42') + end + + def test_sanitize_sql_like_with_custom_escape_character + assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!') + assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!') + assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!') + assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!') + assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!') + end + + def test_sanitize_sql_like_example_use_case + searchable_post = Class.new(Post) do + def self.search(term) + where("title LIKE ?", sanitize_sql_like(term, '!')) + end + end + + assert_sql(/LIKE '20!% !_reduction!_!!'/) do + searchable_post.search("20% _reduction_!").to_a + end + end end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 099e039255..629dfe4765 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -145,6 +145,10 @@ class Post < ActiveRecord::Base has_many :lazy_readers has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader' + has_many :lazy_people, :through => :lazy_readers, :source => :person + has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader' + has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person + def self.top(limit) ranked_by_comments.limit_by(limit) end diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb index 3a6b7fad34..91afc1898c 100644 --- a/activerecord/test/models/reader.rb +++ b/activerecord/test/models/reader.rb @@ -16,6 +16,8 @@ class LazyReader < ActiveRecord::Base self.table_name = "readers" default_scope -> { where(skimmer: true) } + scope :skimmers_or_not, -> { unscope(:where => :skimmer) } + belongs_to :post belongs_to :person end |