Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Let third-party adapters implement their own database rake tasks #5520

Closed
wants to merge 3 commits into from

2 participants

@dazuma

The file activerecord/lib/active_record/railties/databases.rake implements the various database rake tasks (such as db:create, db:structure:dump, etc.). However, it currently recognizes particular ActiveRecord connection adapters by name and hard-codes an implementation for each adapter. This means, if you write (or update) a third-party adapter, you must edit this central file in order to get the database rake tasks to work for your adapter.

This patch includes a mechanism to let ActiveRecord adapters provide their own implementation for the standard rake tasks. An adapter may optionally provide a Tasks object that provides implementations of any number of operations. This object must be distinct from the connection adapter itself because some tasks (such as database creation) cannot be run within the context of any one connection. The object, and any and all operations it provides, are all completely optional, so existing adapters will be fully backwards compatible.

This patch also demonstrates how the mechanism will work, by porting the db:charset task. Previously, databases.rake implemented this task using a case statement on the adapter name. Now, the implementations for the various supported adapters (mysql[2], Postgresql, and sqlite3) have been moved into the adapters themselves via the task mechanism, thus removing the adapter-specific code from databases.rake.

I haven't yet ported the rest of databases.rake because I wanted to get a sign-off on this mechanism before doing that work. If this patch is accepted, I'll go ahead and port the rest of databases.rake. I believe we can drastically reduce the size of databases.rake, and eliminate all the adapter-specific code therein. More importantly, this will allow third-party adapters to provide their own implementations of these tasks without touching the central repository.

@arunagw
Collaborator

Please try to rebase your PR first. Then we can see if this looks good.

@dazuma

Thanks Arun. Sorry, I got really busy, but I will get to rebasing it this week.

@dazuma

Okay, I took a look at recent changes to master, and it looks like someone has put in an alternative solution. I believe it fulfills my use case (that of writing database tasks for new ActiveRecord adapters). Accordingly, I'm closing this pull request. I'll re-open a different pull request if it turns out I need something else.

@dazuma dazuma closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
47 activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -303,6 +303,53 @@ def translate_exception(e, message)
# override in derived class
ActiveRecord::StatementInvalid.new(message)
end
+
+ # A base class for objects that implement auxiliary database
+ # tasks that do not necessarily require that a connection be
+ # established beforehand. Such tasks are useful for rake tasks
+ # and other one-time operations.
+ #
+ # To implement tasks for a particular adapter, subclass this
+ # class and implement methods. The presence or absence of a
+ # method definition, as determined by respond_to?, should be
+ # used to determine whether a given adapter can perform a given
+ # task. Finally, the adapter should implement the
+ # ADAPTERNAME_database_tasks method in the ConnectionHandling
+ # module to create a tasks object given the database config
+ # hash (similar to how the ADAPTERNAME_connection method
+ # creates a connection adapter object.)
+ #
+ # If a task wants to call establish_connection or some other
+ # mechanism that will clobber connection state, it should use
+ # the provided temp_klass, which is an internal subclass of
+ # ActiveRecord::Base, to silo the operation.
+ #
+ # The following are well-known task names (method names) that
+ # can be implemented:
+ #
+ # [<tt>database_encoding</tt>]
+ # Return the database encoding as a string.
+ # Used by rake db:charset.
+ class Tasks
+
+ class TempAR < ActiveRecord::Base # :nodoc:
+ end
+
+ # The db configuration, suitable for establishing a connection
+ attr_reader :config
+
+ # A temporary ActiveRecord class, which can be used to silo a
+ # connection for internal use, to avoid clobbering the state
+ # of any other connections.
+ attr_reader :temp_klass
+
+ def initialize(config)
+ @temp_klass = TempAR
+ @config = config
+ end
+
+ end
+
end
end
end
View
10 activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -4,6 +4,16 @@
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+
+ class Tasks < AbstractAdapter::Tasks
+
+ def database_encoding
+ temp_klass.establish_connection(config)
+ temp_klass.connection.charset
+ end
+
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
attr_reader :collation
View
19 activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -3,14 +3,26 @@ module ConnectionAdapters
class ConnectionSpecification #:nodoc:
attr_reader :config, :adapter_method
- def initialize(config, adapter_method)
- @config, @adapter_method = config, adapter_method
+ def initialize(config, adapter_method, tasks_method=nil)
+ tasks_method ||= adapter_method.to_s.sub(/_connection$/, '_database_tasks')
+ @config, @adapter_method, @tasks_method = config, adapter_method, tasks_method
end
def initialize_dup(original)
@config = original.config.dup
end
+ def database_tasks(klass)
+ unless defined?(@tasks)
+ if klass.respond_to?(@tasks_method)
+ @tasks = klass.send(@tasks_method, @config)
+ else
+ @tasks = nil
+ end
+ end
+ @tasks
+ end
+
##
# Builds a ConnectionSpecification from user input
class Resolver # :nodoc:
@@ -56,8 +68,9 @@ def resolve_hash_connection(spec) # :nodoc:
end
adapter_method = "#{spec[:adapter]}_connection"
+ tasks_method = "#{spec[:adapter]}_database_tasks"
- ConnectionSpecification.new(spec, adapter_method)
+ ConnectionSpecification.new(spec, adapter_method, tasks_method)
end
def connection_url_to_hash(url) # :nodoc:
View
4 activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -17,6 +17,10 @@ def mysql2_connection(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
end
+
+ def mysql2_database_tasks(config) # :nodoc:
+ ConnectionAdapters::AbstractMysqlAdapter::Tasks.new(config)
+ end
end
module ConnectionAdapters
View
4 activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -37,6 +37,10 @@ def mysql_connection(config) # :nodoc:
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
end
+
+ def mysql_database_tasks(config) # :nodoc:
+ ConnectionAdapters::AbstractMysqlAdapter::Tasks.new(config)
+ end
end
module ConnectionAdapters
View
14 activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -30,6 +30,10 @@ def postgresql_connection(config) # :nodoc:
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
end
+
+ def postgresql_database_tasks(config) # :nodoc:
+ ConnectionAdapters::PostgreSQLAdapter::Tasks.new(config)
+ end
end
module ConnectionAdapters
@@ -267,6 +271,16 @@ def simplified_type(field_type)
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
+
+ class Tasks < AbstractAdapter::Tasks
+
+ def database_encoding
+ temp_klass.establish_connection(config)
+ temp_klass.connection.encoding
+ end
+
+ end
+
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
def xml(*args)
options = args.extract_options!
View
4 activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -32,6 +32,10 @@ def sqlite3_connection(config) # :nodoc:
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
+
+ def sqlite3_database_tasks(config) # :nodoc:
+ ConnectionAdapters::SQLiteAdapter::Tasks.new(config)
+ end
end
module ConnectionAdapters #:nodoc:
View
10 activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
@@ -22,6 +22,16 @@ def binary_to_string(value)
#
# * <tt>:database</tt> - Path to the database file.
class SQLiteAdapter < AbstractAdapter
+
+ class Tasks < AbstractAdapter::Tasks
+
+ def database_encoding
+ temp_klass.establish_connection(config)
+ temp_klass.connection.encoding
+ end
+
+ end
+
class Version
include Comparable
View
5 activerecord/lib/active_record/connection_handling.rb
@@ -48,6 +48,11 @@ def establish_connection(spec = ENV["DATABASE_URL"])
connection_handler.establish_connection name, spec
end
+ def database_tasks(spec = ENV["DATABASE_URL"])
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(spec, configurations)
+ resolver.spec.database_tasks(self)
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
View
25 activerecord/lib/active_record/railties/databases.rake
@@ -48,6 +48,12 @@ db_namespace = namespace :db do
end
def create_database(config)
+ # Let adapters implement their own create_database mechanism.
+ tasks = ActiveRecord::Base.database_tasks(config)
+ if tasks.respond_to?(:create_database)
+ return tasks.create_database
+ end
+ # FIXME: move the following implementations into the individual adapters.
begin
if config['adapter'] =~ /sqlite/
if File.exist?(config['database'])
@@ -255,16 +261,9 @@ db_namespace = namespace :db do
# desc "Retrieves the charset for the current environment's database"
task :charset => :environment do
config = ActiveRecord::Base.configurations[Rails.env || 'development']
- case config['adapter']
- when /mysql/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.charset
- when /postgresql/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.encoding
- when /sqlite/
- ActiveRecord::Base.establish_connection(config)
- puts ActiveRecord::Base.connection.encoding
+ tasks = ActiveRecord::Base.database_tasks(config)
+ if tasks.respond_to?(:database_encoding)
+ puts tasks.database_encoding
else
$stderr.puts 'sorry, your database adapter is not supported yet, feel free to submit a patch'
end
@@ -592,6 +591,12 @@ end
task 'test:prepare' => 'db:test:prepare'
def drop_database(config)
+ # Let adapters implement their own drop_database mechanism.
+ tasks = ActiveRecord::Base.database_tasks(config)
+ if tasks.respond_to?(:drop_database)
+ return tasks.drop_database
+ end
+ # FIXME: move the following implementations into the individual adapters.
case config['adapter']
when /mysql/
ActiveRecord::Base.establish_connection(config)
View
41 activerecord/test/cases/adapter_tasks_test.rb
@@ -0,0 +1,41 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AdapterTasksTest < ActiveRecord::TestCase
+
+ def setup
+ @config = ARTest.connection_config
+ end
+
+ test "retrieves a database adapter tasks object" do
+ tasks = ActiveRecord::Model.database_tasks('arunit')
+ if current_adapter?(:SQLiteAdapter)
+ assert_equal('sqlite3', tasks.config[:adapter])
+ elsif current_adapter?(:PostgreSQLAdapter)
+ assert_equal('postgresql', tasks.config[:adapter])
+ elsif current_adapter?(:MysqlAdapter)
+ assert_equal('mysql', tasks.config[:adapter])
+ elsif current_adapter?(:Mysql2Adapter)
+ assert_equal('mysql2', tasks.config[:adapter])
+ else
+ skip "Adapter may not support retrieve_database_adapter"
+ end
+ end
+
+ test "returns database encoding" do
+ tasks = ActiveRecord::Model.database_tasks('arunit')
+ if current_adapter?(:SQLiteAdapter)
+ assert_equal('UTF-8', tasks.database_encoding)
+ elsif current_adapter?(:PostgreSQLAdapter)
+ assert_equal('UTF8', tasks.database_encoding)
+ elsif current_adapter?(:AbstractMysqlAdapter)
+ assert_equal('utf8', tasks.database_encoding)
+ else
+ skip "Adapter may not support database_encoding"
+ end
+ end
+
+ end
+ end
+end
Something went wrong with that request. Please try again.