diff options
Diffstat (limited to 'activerecord')
13 files changed, 269 insertions, 55 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d06a43f0f2..b5d3420dd7 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -5,7 +5,50 @@ *Vipul A M*, *khustochka* -* Deprecated use of string argument as a configuration lookup in `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. +* Add the ability to nullify the `enum` column. + + Example: + + class Conversation < ActiveRecord::Base + enum gender: [:female, :male] + end + + Conversation::GENDER # => { female: 0, male: 1 } + + # conversation.update! gender: 0 + conversation.female! + conversation.female? # => true + conversation.gender # => "female" + + # conversation.update! gender: nil + conversation.gender = nil + conversation.gender.nil? # => true + conversation.gender # => nil + + *Amr Tamimi* + +* Connection specification now accepts a "url" key. The value of this + key is expected to contain a database URL. The database URL will be + expanded into a hash and merged. + + *Richard Schneeman* + +* An `ArgumentError` is now raised on a call to `Relation#where.not(nil)`. + + Example: + + User.where.not(nil) + + # Before + # => 'SELECT `users`.* FROM `users` WHERE (NOT (NULL))' + + # After + # => ArgumentError, 'Invalid argument for .where.not(), got nil.' + + *Kuldeep Aggarwal* + +* Deprecated use of string argument as a configuration lookup in + `ActiveRecord::Base.establish_connection`. Instead, a symbol must be given. *José Valim* @@ -41,10 +84,10 @@ *Richard Schneeman* -* Do not raise `'can not touch on a new record object'` exception on destroying already destroyed - `belongs_to` association with `touch: true` option +* Do not raise `'can not touch on a new record object'` exception on destroying + already destroyed `belongs_to` association with `touch: true` option. - Fixes: #13445 + Fixes #13445. Example: diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 9506960be3..295dccf34e 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -114,7 +114,7 @@ module ActiveRecord walk join_root, oj.join_root else oj.join_root.children.flat_map { |child| - make_outer_joins join_root, child + make_outer_joins oj.join_root, child } end } diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 7e16156408..049768effc 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -13,15 +13,113 @@ module ActiveRecord @config = original.config.dup end + # Expands a connection string into a hash. + class ConnectionUrlResolver # :nodoc: + + # == Example + # + # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000" + # ConnectionUrlResolver.new(url).to_hash + # # => { + # "adapter" => "postgresql", + # "host" => "localhost", + # "port" => 9000, + # "database" => "foo_test", + # "username" => "foo", + # "password" => "bar", + # "pool" => "5", + # "timeout" => "3000" + # } + def initialize(url) + raise "Database URL cannot be empty" if url.blank? + @uri = URI.parse(url) + @adapter = @uri.scheme + @adapter = "postgresql" if @adapter == "postgres" + @query = @uri.query || '' + end + + # Converts the given URL to a full connection hash. + def to_hash + config = raw_config.reject { |_,value| value.blank? } + config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config + end + + private + + def uri + @uri + end + + def uri_parser + @uri_parser ||= URI::Parser.new + end + + # Converts the query parameters of the URI into a hash. + # + # "localhost?pool=5&reap_frequency=2" + # # => { "pool" => "5", "reap_frequency" => "2" } + # + # returns empty hash if no query present. + # + # "localhost" + # # => {} + def query_hash + Hash[@query.split("&").map { |pair| pair.split("=") }] + end + + def raw_config + query_hash.merge({ + "adapter" => @adapter, + "username" => uri.user, + "password" => uri.password, + "port" => uri.port, + "database" => database, + "host" => uri.host }) + end + + # Returns name of the database. + # Sqlite3 expects this to be a full path or `:memory`. + def database + if @adapter == 'sqlite3' + if '/:memory:' == uri.path + ':memory:' + else + uri.path + end + else + uri.path.sub(%r{^/},"") + end + end + end + ## - # Builds a ConnectionSpecification from user input + # Builds a ConnectionSpecification from user input. class Resolver # :nodoc: attr_reader :configurations + # Accepts a hash two layers deep, keys on the first layer represent + # environments such as "production". Keys must be strings. def initialize(configurations) @configurations = configurations end + # Returns a hash with database connection information. + # + # == Examples + # + # Full hash Configuration. + # + # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } + # Resolver.new(configurations).resolve(:production) + # # => {host: "localhost", database: "foo", adapter: "sqlite3"} + # + # Initialized with URL configuration strings. + # + # configurations = { "production" => "postgresql://localhost/foo" } + # Resolver.new(configurations).resolve(:production) + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # def resolve(config) if config resolve_connection config @@ -32,6 +130,18 @@ module ActiveRecord end end + # Returns an instance of ConnectionSpecification for a given adapter. + # Accepts a hash one layer deep that contains all connection information. + # + # == Example + # + # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } + # spec = Resolver.new(config).spec(:production) + # spec.adapter_method + # # => "sqlite3" + # spec.config + # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } + # def spec(config) spec = resolve(config).symbolize_keys @@ -52,7 +162,27 @@ module ActiveRecord private - def resolve_connection(spec) #:nodoc: + # Returns fully resolved connection, accepts hash, string or symbol. + # Always returns a hash. + # + # == Examples + # + # Symbol representing current environment. + # + # Resolver.new("production" => {}).resolve_connection(:production) + # # => {} + # + # One layer deep hash of connection values. + # + # Resolver.new({}).resolve_connection("adapter" => "sqlite3") + # # => { "adapter" => "sqlite3" } + # + # Connection URL. + # + # Resolver.new({}).resolve_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_connection(spec) case spec when Symbol, String resolve_env_connection spec @@ -61,9 +191,22 @@ module ActiveRecord end end - def resolve_env_connection(spec) # :nodoc: + # Takes the environment such as `:production` or `:development`. + # This requires that the @configurations was initialized with a key that + # matches. + # + # + # Resolver.new("production" => {}).resolve_env_connection(:production) + # # => {} + # + # Takes a connection URL. + # + # Resolver.new({}).resolve_env_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # + def resolve_env_connection(spec) # Rails has historically accepted a string to mean either - # an environment key or a url spec, so we have deprecated + # an environment key or a URL spec, so we have deprecated # this ambiguous behaviour and in the future this function # can be removed in favor of resolve_string_connection and # resolve_symbol_connection. @@ -80,44 +223,20 @@ module ActiveRecord end end - def resolve_hash_connection(spec) # :nodoc: + # Accepts a hash. Expands the "url" key that contains a + # URL database connection to a full connection + # hash and merges with the rest of the hash. + # Connection details inside of the "url" key win any merge conflicts + def resolve_hash_connection(spec) + if url = spec.delete("url") + connection_hash = resolve_string_connection(url) + spec.merge!(connection_hash) + end spec end - def resolve_string_connection(spec) # :nodoc: - config = URI.parse spec - adapter = config.scheme - adapter = "postgresql" if adapter == "postgres" - - database = if adapter == 'sqlite3' - if '/:memory:' == config.path - ':memory:' - else - config.path - end - else - config.path.sub(%r{^/},"") - end - - spec = { "adapter" => adapter, - "username" => config.user, - "password" => config.password, - "port" => config.port, - "database" => database, - "host" => config.host } - - spec.reject!{ |_,value| value.blank? } - - uri_parser = URI::Parser.new - - spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) } - - if config.query - options = Hash[config.query.split("&").map{ |pair| pair.split("=") }] - spec.merge!(options) - end - - spec + def resolve_string_connection(url) + ConnectionUrlResolver.new(url).to_hash end end end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 2f8439892b..1d484f7c15 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -18,6 +18,11 @@ module ActiveRecord # # conversation.update! status: 1 # conversation.status = "archived" # + # # conversation.update! status: nil + # conversation.status = nil + # conversation.status.nil? # => true + # conversation.status # => nil + # # You can set the default value from the database declaration, like: # # create_table :conversations do |t| @@ -62,7 +67,7 @@ module ActiveRecord _enum_methods_module.module_eval do # def status=(value) self[:status] = STATUS[value] end define_method("#{name}=") { |value| - unless enum_values.has_key?(value) + unless enum_values.has_key?(value) || value.blank? raise ArgumentError, "'#{value}' is not a valid #{name}" end self[name] = enum_values[value] diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 90dfc177e5..4b769aa11c 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -179,9 +179,6 @@ db_namespace = namespace :db do require 'active_record/fixtures' base_dir = if ENV['FIXTURES_PATH'] - STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + - "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + - "instead." File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten else ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -204,9 +201,6 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label base_dir = if ENV['FIXTURES_PATH'] - STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " + - "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " + - "instead." File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten else ActiveRecord::Tasks::DatabaseTasks.fixtures_path diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 3d0709266a..78da6a83ec 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -37,6 +37,8 @@ module ActiveRecord def not(opts, *rest) where_value = @scope.send(:build_where, opts, rest).map do |rel| case rel + when NilClass + raise ArgumentError, 'Invalid argument for .where.not(), got nil.' when Arel::Nodes::In Arel::Nodes::NotIn.new(rel.left, rel.right) when Arel::Nodes::Equality diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index a85f3d4298..578f6301bd 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -18,7 +18,8 @@ module ActiveRecord def test_bad_connection_mysql assert_raise ActiveRecord::NoDatabaseError do - connection = ActiveRecord::Base.mysql_connection(adapter: "mysql", database: "should_not_exist-cinco-dog-db") + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') + connection = ActiveRecord::Base.mysql_connection(configuration) connection.exec_query('drop table if exists ex') end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 91b780a7ee..9b7202c915 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -15,7 +15,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do - connection = ActiveRecord::Base.mysql2_connection(adapter: "mysql2", database: "should_not_exist-cinco-dog-db") + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') + connection = ActiveRecord::Base.mysql2_connection(configuration) connection.exec_query('drop table if exists ex') end end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 778175e2e8..131080913c 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -12,7 +12,8 @@ module ActiveRecord def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do - connection = ActiveRecord::Base.postgresql_connection(database: "should_not_exist-cinco-dog-db", adapter: "postgresql") + configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db') + connection = ActiveRecord::Base.postgresql_connection(configuration) connection.exec_query('drop table if exists ex') end end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index ba8440a16b..fdd1914cba 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -31,6 +31,24 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_url_sub_key + spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'} + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8" }, spec) + end + + def test_url_sub_key_merges_correctly + hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"} + spec = resolve :production, 'production' => hash + assert_equal({ + "adapter" => "abstract", + "host" => "foo", + "encoding" => "utf8", + "pool" => "3" }, spec) + end + def test_url_host_no_db spec = resolve 'abstract://foo?encoding=utf8' assert_equal({ diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 35e8a98156..017edcb194 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -58,6 +58,21 @@ class EnumTest < ActiveRecord::TestCase assert_equal "'unknown' is not a valid status", e.message end + test "assign nil value" do + @book.status = nil + assert @book.status.nil? + end + + test "assign empty string value" do + @book.status = '' + assert @book.status.nil? + end + + test "assign long empty string value" do + @book.status = ' ' + assert @book.status.nil? + end + test "constant to access the mapping" do assert_equal 0, Book::STATUS[:proposed] assert_equal 1, Book::STATUS["written"] diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index d44b4dfe3d..fd2420cb88 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -23,6 +23,12 @@ module ActiveRecord assert_equal([expected], relation.where_values) end + def test_not_with_nil + assert_raise ArgumentError do + Post.where.not(nil) + end + end + def test_not_in expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %w[hello goodbye]) relation = Post.where.not(title: %w[hello goodbye]) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 70d113fb39..15611656fd 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -207,6 +207,16 @@ module ActiveRecord assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent + post = Post.create!(title: "haha", body: "huhu") + comment = post.comments.create!(body: "hu") + 3.times { comment.ratings.create! } + + relation = Post.joins(:comments).merge Comment.joins(:ratings) + + assert_equal 3, relation.where(id: post.id).pluck(:id).size + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" @@ -221,6 +231,5 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end - end end |