Skip to content

Commit 9c22f35

Browse files
authored
Merge pull request rails#50140 from kmcphillips/ar-protocol-adapter
Add a `ActiveRecord.protocol_adapters` configuration to map `DATABASE_URL` protocols to adapters at an application level
2 parents d22c657 + 0ccf300 commit 9c22f35

File tree

5 files changed

+119
-2
lines changed

5 files changed

+119
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
* When using a `DATABASE_URL`, allow for a configuration to map the protocol in the URL to a specific database
2+
adapter. This allows decoupling the adapter the application chooses to use from the database connection details
3+
set in the deployment environment.
4+
5+
```ruby
6+
# ENV['DATABASE_URL'] = "mysql://localhost/example_database"
7+
config.active_record.protocol_adapters.mysql = "trilogy"
8+
# will connect to MySQL using the trilogy adapter
9+
```
10+
11+
*Jean Boussier*, *Kevin McPhillips*
12+
113
* In cases where MySQL returns `warning_count` greater than zero, but returns no warnings when
214
the `SHOW WARNINGS` query is executed, `ActiveRecord.db_warnings_action` proc will still be
315
called with a generic warning message rather than silently ignoring the warning(s).

activerecord/lib/active_record.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
require "active_support"
2727
require "active_support/rails"
28+
require "active_support/ordered_options"
2829
require "active_model"
2930
require "arel"
3031
require "yaml"
@@ -464,6 +465,34 @@ def self.marshalling_format_version=(value)
464465
Marshalling.format_version = value
465466
end
466467

468+
##
469+
# :singleton-method:
470+
# Provides a mapping between database protocols/DBMSs and the
471+
# underlying database adapter to be used. This is used only by the
472+
# <tt>DATABASE_URL</tt> environment variable.
473+
#
474+
# == Example
475+
#
476+
# DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase"
477+
#
478+
# The above URL specifies that MySQL is the desired protocol/DBMS, and the
479+
# application configuration can then decide which adapter to use. For this example
480+
# the default mapping is from <tt>mysql</tt> to <tt>mysql2</tt>, but <tt>:trilogy</tt>
481+
# is also supported.
482+
#
483+
# ActiveRecord.protocol_adapters.mysql = "mysql2"
484+
#
485+
# The protocols names are arbitrary, and external database adapters can be
486+
# registered and set here.
487+
singleton_class.attr_accessor :protocol_adapters
488+
self.protocol_adapters = ActiveSupport::InheritableOptions.new(
489+
{
490+
sqlite: "sqlite3",
491+
mysql: "mysql2",
492+
postgres: "postgresql",
493+
}
494+
)
495+
467496
def self.eager_load!
468497
super
469498
ActiveRecord::Locking.eager_load!

activerecord/lib/active_record/database_configurations/connection_url_resolver.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ class ConnectionUrlResolver # :nodoc:
2525
def initialize(url)
2626
raise "Database URL cannot be empty" if url.blank?
2727
@uri = uri_parser.parse(url)
28-
@adapter = @uri.scheme && @uri.scheme.tr("-", "_")
29-
@adapter = "postgresql" if @adapter == "postgres"
28+
@adapter = resolved_adapter
3029

3130
if @uri.opaque
3231
@uri.opaque, @query = @uri.opaque.split("?", 2)
@@ -80,6 +79,12 @@ def raw_config
8079
end
8180
end
8281

82+
def resolved_adapter
83+
adapter = uri.scheme && @uri.scheme.tr("-", "_")
84+
adapter = ActiveRecord.protocol_adapters[adapter] || adapter
85+
adapter
86+
end
87+
8388
# Returns name of the database.
8489
def database_from_path
8590
if @adapter == "sqlite3"

activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ def setup
1010
@previous_rack_env = ENV.delete("RACK_ENV")
1111
@previous_rails_env = ENV.delete("RAILS_ENV")
1212
@adapters_was = ActiveRecord::ConnectionAdapters.instance_variable_get(:@adapters).dup
13+
@protocol_adapters = ActiveRecord.protocol_adapters.dup
1314
end
1415

1516
teardown do
1617
ENV["DATABASE_URL"] = @previous_database_url
1718
ENV["RACK_ENV"] = @previous_rack_env
1819
ENV["RAILS_ENV"] = @previous_rails_env
1920
ActiveRecord::ConnectionAdapters.instance_variable_set(:@adapters, @adapters_was)
21+
ActiveRecord.protocol_adapters = @protocol_adapters
2022
end
2123

2224
def resolve_config(config, env_name = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call)
@@ -434,6 +436,59 @@ def test_does_not_change_other_environments
434436
adapter: "postgresql",
435437
}, actual.configuration_hash)
436438
end
439+
440+
def test_protocol_adapter_mapping_is_used
441+
ENV["DATABASE_URL"] = "mysql://localhost/exampledb"
442+
ENV["RAILS_ENV"] = "production"
443+
444+
actual = resolve_db_config(:production, {})
445+
expected = { adapter: "mysql2", database: "exampledb", host: "localhost" }
446+
447+
assert_equal expected, actual.configuration_hash
448+
end
449+
450+
def test_protocol_adapter_mapping_falls_through_if_non_found
451+
ENV["DATABASE_URL"] = "unknown://localhost/exampledb"
452+
ENV["RAILS_ENV"] = "production"
453+
454+
actual = resolve_db_config(:production, {})
455+
expected = { adapter: "unknown", database: "exampledb", host: "localhost" }
456+
457+
assert_equal expected, actual.configuration_hash
458+
end
459+
460+
def test_protocol_adapter_mapping_is_used_and_can_be_updated
461+
ActiveRecord.protocol_adapters.potato = "postgresql"
462+
ENV["DATABASE_URL"] = "potato://localhost/exampledb"
463+
ENV["RAILS_ENV"] = "production"
464+
465+
actual = resolve_db_config(:production, {})
466+
expected = { adapter: "postgresql", database: "exampledb", host: "localhost" }
467+
468+
assert_equal expected, actual.configuration_hash
469+
end
470+
471+
def test_protocol_adapter_mapping_translates_underscores_to_dashes
472+
ActiveRecord.protocol_adapters.custom_protocol = "postgresql"
473+
ENV["DATABASE_URL"] = "custom-protocol://localhost/exampledb"
474+
ENV["RAILS_ENV"] = "production"
475+
476+
actual = resolve_db_config(:production, {})
477+
expected = { adapter: "postgresql", database: "exampledb", host: "localhost" }
478+
479+
assert_equal expected, actual.configuration_hash
480+
end
481+
482+
def test_protocol_adapter_mapping_handles_sqlite3_file_urls
483+
ActiveRecord.protocol_adapters.custom_protocol = "sqlite3"
484+
ENV["DATABASE_URL"] = "custom-protocol:/path/to/db.sqlite3"
485+
ENV["RAILS_ENV"] = "production"
486+
487+
actual = resolve_db_config(:production, {})
488+
expected = { adapter: "sqlite3", database: "/path/to/db.sqlite3" }
489+
490+
assert_equal expected, actual.configuration_hash
491+
end
437492
end
438493
end
439494
end

guides/source/configuring.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,18 @@ The default value depends on the `config.load_defaults` target version:
16401640
| (original) | `true` |
16411641
| 7.1 | `false` |
16421642
1643+
#### `config.active_record.protocol_adapters`
1644+
1645+
When using a URL to configure the database connection, this option provides a mapping from the protocol to the underlying
1646+
database adapter. For example, this means the environment can specify `DATABASE_URL=mysql://localhost/database` and Rails will map
1647+
`mysql` to the `mysql2` adapter, but the application can also override these mappings:
1648+
1649+
```ruby
1650+
config.active_record.protocol_adapters.mysql = "trilogy"
1651+
```
1652+
1653+
If no mapping is found, the protocol is used as the adapter name.
1654+
16431655
### Configuring Action Controller
16441656
16451657
`config.action_controller` includes a number of configuration settings:
@@ -2950,6 +2962,10 @@ development:
29502962
29512963
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.
29522964
2965+
When using a `ENV['DATABASE_URL']` or a `url` key in your `config/database.yml` file, Rails allows mapping the protocol
2966+
in the URL to a database adapter that can be configured from within the application. This allows the adapter to be configured
2967+
without modifying the URL set in the deployment environment. See: [`config.active_record.protocol_adapters`](#config-active_record-protocol-adapters).
2968+
29532969
29542970
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.
29552971

0 commit comments

Comments
 (0)