diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a39c056bbe..751d2c2a63 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* When using a `DATABASE_URL`, allow for a configuration to map the protocol in the URL to a specific database + adapter. This allows decoupling the adapter the application chooses to use from the database connection details + set in the deployment environment. + + ```ruby + # ENV['DATABASE_URL'] = "mysql://localhost/example_database" + config.active_record.protocol_adapters.mysql = "trilogy" + # will connect to MySQL using the trilogy adapter + ``` + + *Jean Boussier*, *Kevin McPhillips* + * In cases where MySQL returns `warning_count` greater than zero, but returns no warnings when the `SHOW WARNINGS` query is executed, `ActiveRecord.db_warnings_action` proc will still be called with a generic warning message rather than silently ignoring the warning(s). diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 6f31055991..41aa1de0f3 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -25,6 +25,7 @@ require "active_support" require "active_support/rails" +require "active_support/ordered_options" require "active_model" require "arel" require "yaml" @@ -464,6 +465,34 @@ def self.marshalling_format_version=(value) Marshalling.format_version = value end + ## + # :singleton-method: + # Provides a mapping between database protocols/DBMSs and the + # underlying database adapter to be used. This is used only by the + # DATABASE_URL environment variable. + # + # == Example + # + # DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase" + # + # The above URL specifies that MySQL is the desired protocol/DBMS, and the + # application configuration can then decide which adapter to use. For this example + # the default mapping is from mysql to mysql2, but :trilogy + # is also supported. + # + # ActiveRecord.protocol_adapters.mysql = "mysql2" + # + # The protocols names are arbitrary, and external database adapters can be + # registered and set here. + singleton_class.attr_accessor :protocol_adapters + self.protocol_adapters = ActiveSupport::InheritableOptions.new( + { + sqlite: "sqlite3", + mysql: "mysql2", + postgres: "postgresql", + } + ) + def self.eager_load! super ActiveRecord::Locking.eager_load! diff --git a/activerecord/lib/active_record/database_configurations/connection_url_resolver.rb b/activerecord/lib/active_record/database_configurations/connection_url_resolver.rb index 6ea9315f9a..6333e819f9 100644 --- a/activerecord/lib/active_record/database_configurations/connection_url_resolver.rb +++ b/activerecord/lib/active_record/database_configurations/connection_url_resolver.rb @@ -25,8 +25,7 @@ class ConnectionUrlResolver # :nodoc: def initialize(url) raise "Database URL cannot be empty" if url.blank? @uri = uri_parser.parse(url) - @adapter = @uri.scheme && @uri.scheme.tr("-", "_") - @adapter = "postgresql" if @adapter == "postgres" + @adapter = resolved_adapter if @uri.opaque @uri.opaque, @query = @uri.opaque.split("?", 2) @@ -80,6 +79,12 @@ def raw_config end end + def resolved_adapter + adapter = uri.scheme && @uri.scheme.tr("-", "_") + adapter = ActiveRecord.protocol_adapters[adapter] || adapter + adapter + end + # Returns name of the database. def database_from_path if @adapter == "sqlite3" 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 index 061e1a91e9..bd8fa5a9b0 100644 --- 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 @@ -10,6 +10,7 @@ def setup @previous_rack_env = ENV.delete("RACK_ENV") @previous_rails_env = ENV.delete("RAILS_ENV") @adapters_was = ActiveRecord::ConnectionAdapters.instance_variable_get(:@adapters).dup + @protocol_adapters = ActiveRecord.protocol_adapters.dup end teardown do @@ -17,6 +18,7 @@ def setup ENV["RACK_ENV"] = @previous_rack_env ENV["RAILS_ENV"] = @previous_rails_env ActiveRecord::ConnectionAdapters.instance_variable_set(:@adapters, @adapters_was) + ActiveRecord.protocol_adapters = @protocol_adapters end def resolve_config(config, env_name = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call) @@ -434,6 +436,59 @@ def test_does_not_change_other_environments adapter: "postgresql", }, actual.configuration_hash) end + + def test_protocol_adapter_mapping_is_used + ENV["DATABASE_URL"] = "mysql://localhost/exampledb" + ENV["RAILS_ENV"] = "production" + + actual = resolve_db_config(:production, {}) + expected = { adapter: "mysql2", database: "exampledb", host: "localhost" } + + assert_equal expected, actual.configuration_hash + end + + def test_protocol_adapter_mapping_falls_through_if_non_found + ENV["DATABASE_URL"] = "unknown://localhost/exampledb" + ENV["RAILS_ENV"] = "production" + + actual = resolve_db_config(:production, {}) + expected = { adapter: "unknown", database: "exampledb", host: "localhost" } + + assert_equal expected, actual.configuration_hash + end + + def test_protocol_adapter_mapping_is_used_and_can_be_updated + ActiveRecord.protocol_adapters.potato = "postgresql" + ENV["DATABASE_URL"] = "potato://localhost/exampledb" + ENV["RAILS_ENV"] = "production" + + actual = resolve_db_config(:production, {}) + expected = { adapter: "postgresql", database: "exampledb", host: "localhost" } + + assert_equal expected, actual.configuration_hash + end + + def test_protocol_adapter_mapping_translates_underscores_to_dashes + ActiveRecord.protocol_adapters.custom_protocol = "postgresql" + ENV["DATABASE_URL"] = "custom-protocol://localhost/exampledb" + ENV["RAILS_ENV"] = "production" + + actual = resolve_db_config(:production, {}) + expected = { adapter: "postgresql", database: "exampledb", host: "localhost" } + + assert_equal expected, actual.configuration_hash + end + + def test_protocol_adapter_mapping_handles_sqlite3_file_urls + ActiveRecord.protocol_adapters.custom_protocol = "sqlite3" + ENV["DATABASE_URL"] = "custom-protocol:/path/to/db.sqlite3" + ENV["RAILS_ENV"] = "production" + + actual = resolve_db_config(:production, {}) + expected = { adapter: "sqlite3", database: "/path/to/db.sqlite3" } + + assert_equal expected, actual.configuration_hash + end end end end diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 8d4a77a8f4..681be8cc0a 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -1640,6 +1640,18 @@ The default value depends on the `config.load_defaults` target version: | (original) | `true` | | 7.1 | `false` | +#### `config.active_record.protocol_adapters` + +When using a URL to configure the database connection, this option provides a mapping from the protocol to the underlying +database adapter. For example, this means the environment can specify `DATABASE_URL=mysql://localhost/database` and Rails will map +`mysql` to the `mysql2` adapter, but the application can also override these mappings: + +```ruby +config.active_record.protocol_adapters.mysql = "trilogy" +``` + +If no mapping is found, the protocol is used as the adapter name. + ### Configuring Action Controller `config.action_controller` includes a number of configuration settings: @@ -2950,6 +2962,10 @@ development: The `config/database.yml` file can contain ERB tags `<%= %>`. Anything in the tags will be evaluated as Ruby code. You can use this to pull out data from an environment variable or to perform calculations to generate the needed connection information. +When using a `ENV['DATABASE_URL']` or a `url` key in your `config/database.yml` file, Rails allows mapping the protocol +in the URL to a database adapter that can be configured from within the application. This allows the adapter to be configured +without modifying the URL set in the deployment environment. See: [`config.active_record.protocol_adapters`](#config-active_record-protocol-adapters). + TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below.