diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index a39c056bbeb..751d2c2a637 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 6f310559917..41aa1de0f39 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 @@ module ActiveRecord
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 6ea9315f9a7..6333e819f9f 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 @@ module ActiveRecord
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 @@ module ActiveRecord
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 061e1a91e9b..bd8fa5a9b0d 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 @@ module ActiveRecord
@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 @@ module ActiveRecord
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 @@ module ActiveRecord
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 8d4a77a8f41..681be8cc0a4 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.