diff options
Diffstat (limited to 'activerecord/lib/active_record')
5 files changed, 167 insertions, 47 deletions
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 |