[activerecord] Better loading pattern for connection adapters that play nicely with bundler --standalone

Hi,

I initially sent this idea as a github
issue: https://github.com/rails/rails/issues/7753 (closed)

Steve Klabnik suggested I should post here first (better late than never).

I've since created a PR: https://github.com/rails/rails/pull/7755

Original discussion:

I'm using the relatively new --standalone feature/option of bundler so as
to avoid extracting a part of my rails project into a separate project. My
only requirement is to not have to load rails entirely:

By creating a custom gemfile (subset of your typical gemfile) and executing:

    bundle install --gemfile=Gemfile.my_subset --standalone

bundler will create a ruby script which requires all the gems in your
gemfile subset using the
original gems locations.

e.g. :

    path = File.expand_path('..', __FILE__)
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/rake-0.9.2.2/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/multi_json-1.0.4/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activesupport-3.1.6/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/builder-3.0.3/lib")
    $:.unshift File.expand_path("#{path}/../ruby/1.9.1/gems/i18n-0.6.0/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activemodel-3.1.6/lib")
    $:.unshift File.expand_path("#{path}/../ruby/1.9.1/gems/arel-2.2.3/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/tzinfo-0.3.33/lib")
    $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activerecord-3.1.6/lib")
    ...

This allows scripts to e.g. load just activerecord without setting up a
separate
gemfile.

The issue here is that with this setup script, you aren't using bundler (to
avoid loading rails and
the gems in your typical gemfile.). Additionally, the gems the script
requires are `expanded` gems
and have not been installed via `gem install`.

Activerecord connection adapters have this code:

    gem 'mysql2', '~> 0.3.10'
    require 'mysql2'

This code assumes:

  * You're running the code within bundler (recommended)
  * You have the mysql2 gem installed as a gem in your system

When using bundler standalone neither of these cases are true so the app
breaks with:

   Please install the mysql2 adapter: `gem install
activerecord-mysql2-adapter` (no such file to load --
active_record/connection_adapters/mysql2_adapter)
because it hits the 'gem' call before it attempts to require 'mysql2'

IMHO a better approach would be:

    begin
      require 'mysql2'
    rescue LoadError
      gem 'mysql2', '~> 0.3.10'
      require 'mysql2'
    end

This code assumes you already have mysql2 on your load path somehow and
only if it raises an exception do we attempt to issue the `gem` call.

If you have mysql2 on your system, the `gem` call will never be made
(defeating the purpose of the `gem` call in the first place).

Is this reasonable?

If we don't use the `gem` command, we can't enforce a particular version
of mysql2 in the adapter code. Since AR has "soft dependencies"
(dependencies that are not declared in the gemspec), we need some way to
ensure that we've loaded a connection handling gem that has the version
we actually support. Doing a `require 'mysql2'` could pick up *any*
version of mysql2, including one that won't actually work with the
AR adapter code.

Hi Aaron,

Hi,

I initially sent this idea as a github
issue: https://github.com/rails/rails/issues/7753 (closed)

Steve Klabnik suggested I should post here first (better late than never).

I've since created a PR: https://github.com/rails/rails/pull/7755

Original discussion:

I'm using the relatively new --standalone feature/option of bundler so as
to avoid extracting a part of my rails project into a separate project. My
only requirement is to not have to load rails entirely:

By creating a custom gemfile (subset of your typical gemfile) and executing:

   bundle install --gemfile=Gemfile.my_subset --standalone

bundler will create a ruby script which requires all the gems in your
gemfile subset using the
original gems locations.

e.g. :

   path = File.expand_path('..', __FILE__)
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/rake-0.9.2.2/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/multi_json-1.0.4/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activesupport-3.1.6/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/builder-3.0.3/lib")
   $:.unshift File.expand_path("#{path}/../ruby/1.9.1/gems/i18n-0.6.0/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activemodel-3.1.6/lib")
   $:.unshift File.expand_path("#{path}/../ruby/1.9.1/gems/arel-2.2.3/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/tzinfo-0.3.33/lib")
   $:.unshift
File.expand_path("#{path}/../ruby/1.9.1/gems/activerecord-3.1.6/lib")
   ...

This allows scripts to e.g. load just activerecord without setting up a
separate
gemfile.

The issue here is that with this setup script, you aren't using bundler (to
avoid loading rails and
the gems in your typical gemfile.). Additionally, the gems the script
requires are `expanded` gems
and have not been installed via `gem install`.

Activerecord connection adapters have this code:

   gem 'mysql2', '~> 0.3.10'
   require 'mysql2'

This code assumes:

* You're running the code within bundler (recommended)
* You have the mysql2 gem installed as a gem in your system

When using bundler standalone neither of these cases are true so the app
breaks with:

  Please install the mysql2 adapter: `gem install
activerecord-mysql2-adapter` (no such file to load --
active_record/connection_adapters/mysql2_adapter)
because it hits the 'gem' call before it attempts to require 'mysql2'

IMHO a better approach would be:

   begin
     require 'mysql2'
   rescue LoadError
     gem 'mysql2', '~> 0.3.10'
     require 'mysql2'
   end

This code assumes you already have mysql2 on your load path somehow and
only if it raises an exception do we attempt to issue the `gem` call.

If you have mysql2 on your system, the `gem` call will never be made
(defeating the purpose of the `gem` call in the first place).

Is this reasonable?

If we don't use the `gem` command, we can't enforce a particular version
of mysql2 in the adapter code. Since AR has "soft dependencies"
(dependencies that are not declared in the gemspec), we need some way to
ensure that we've loaded a connection handling gem that has the version
we actually support. Doing a `require 'mysql2'` could pick up *any*
version of mysql2, including one that won't actually work with the
AR adapter code.

I understand though I think it sucks…

What about something like:

    begin
       gem 'mysql2', '~> 0.3.11'
       require 'mysql2'
    rescue LoadError
      puts "You do not appear to have mysql2 (~> 0.3.11) installed. AR requires this to function correctly."

      begin
        require 'mysql2'
        puts "Found some version of mysql2. Proceeding..."
      rescue LoadError => err
        raise err
      end
    end

Then you leave the responsibility up to the developer…

--
Aaron Patterson
http://tenderlovemaking.com/

Saimon