From 03a36fb4f1c8039e3580a4b450f2b3c1dfe4b30e Mon Sep 17 00:00:00 2001 From: Andrew Kane Date: Wed, 1 Nov 2017 22:16:09 -0700 Subject: [PATCH] First commit --- .gitignore | 9 ++ CHANGELOG.md | 3 + Gemfile | 4 + LICENSE.txt | 22 ++++ README.md | 109 ++++++++++++++++++ Rakefile | 10 ++ lib/generators/multiverse/db_generator.rb | 35 ++++++ lib/generators/multiverse/templates/record.rb | 4 + lib/multiverse.rb | 41 +++++++ lib/multiverse/generators.rb | 15 +++ lib/multiverse/patches.rb | 46 ++++++++ lib/multiverse/railtie.rb | 22 ++++ lib/multiverse/version.rb | 3 + multiverse.gemspec | 27 +++++ test/multiverse_test.rb | 7 ++ test/test_helper.rb | 4 + 16 files changed, 361 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 Rakefile create mode 100644 lib/generators/multiverse/db_generator.rb create mode 100644 lib/generators/multiverse/templates/record.rb create mode 100644 lib/multiverse.rb create mode 100644 lib/multiverse/generators.rb create mode 100644 lib/multiverse/patches.rb create mode 100644 lib/multiverse/railtie.rb create mode 100644 lib/multiverse/version.rb create mode 100644 multiverse.gemspec create mode 100644 test/multiverse_test.rb create mode 100644 test/test_helper.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a596cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +*.lock diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ec5864e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +- First release diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..270fa42 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in multiverse.gemspec +gemspec diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2e321ea --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2017 Andrew Kane + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7ba38d --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Multiverse + +:fire: Multiple databases for Rails + +One of the easiest ways to scale your database is to move large, infrequently-joined tables to a separate database. **ActiveRecord supports multiple databases, but Rails doesn’t provide a way to manage them.** Multiverse changes this. + +Works with Rails 5+ + +## Installation + +Add this line to your application’s Gemfile: + +```ruby +gem 'multiverse' +``` + +## Getting Started + +Generate a new database + +```sh +rails generate multiverse:db catalog +``` + +This generates `CatalogRecord` class for models to inherit from and adds configuration to `config/database.yml`. It also creates a `db/catalog` directory for migrations and `schema.rb` to live. + +`rails` and `rake` commands will run for the original database by default. To run commands for the new database, use the `DB` environment variable. For instance: + +Create the database + +```sh +DB=catalog rake db:create +``` + +Create a migration + +```sh +DB=catalog rails generate migration add_name_to_products +``` + +Run migrations + +```sh +DB=catalog rake db:migrate +``` + +Rollback + +```sh +DB=catalog rake db:rollback +``` + +## Models + +Also works for models + +```sh +DB=catalog rails generate model Product +``` + +This generates + +```rb +class Product < CatalogRecord +end +``` + +## Seeds + +Use `db/seeds.rb` for all databases + +## Web Servers + +### Puma + +Add to `config/puma.rb` + +```ruby +on_worker_boot do + CatalogRecord.establish_connection :"catalog_#{Rails.env}" +end +``` + +### Unicorn + +Add to `config/unicorn.rb` + +```ruby +before_fork do |server, worker| + CatalogRecord.connection.disconnect! +end + +after_fork do |server, worker| + CatalogRecord.establish_connection :"catalog_#{Rails.env}" +end +``` + +## History + +View the [changelog](https://github.com/ankane/multiverse/blob/master/CHANGELOG.md) + +## Contributing + +Everyone is encouraged to help improve this project. Here are a few ways you can help: + +- [Report bugs](https://github.com/ankane/multiverse/issues) +- Fix bugs and [submit pull requests](https://github.com/ankane/multiverse/pulls) +- Write, clarify, or fix documentation +- Suggest or add new features diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..d433a1e --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +require "bundler/gem_tasks" +require "rake/testtask" + +Rake::TestTask.new(:test) do |t| + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] +end + +task :default => :test diff --git a/lib/generators/multiverse/db_generator.rb b/lib/generators/multiverse/db_generator.rb new file mode 100644 index 0000000..f61fb47 --- /dev/null +++ b/lib/generators/multiverse/db_generator.rb @@ -0,0 +1,35 @@ +require "rails/generators" + +module Multiverse + module Generators + class DbGenerator < Rails::Generators::Base + source_root File.expand_path("../templates", __FILE__) + + argument :name + + def create_initializer + lower_name = name.underscore + + template "record.rb", "app/models/#{lower_name}_record.rb" + + append_to_file "config/database.yml" do + " +#{name}_development: + <<: *default + database: #{lower_name}_development + +#{name}_test: + <<: *default + database: #{lower_name}_test + +#{name}_production: + <<: *default + url: <%= ENV['#{lower_name.upcase}_DATABASE_URL'] %> +" + end + + empty_directory "db/#{lower_name}/migrate" + end + end + end +end diff --git a/lib/generators/multiverse/templates/record.rb b/lib/generators/multiverse/templates/record.rb new file mode 100644 index 0000000..5ea89eb --- /dev/null +++ b/lib/generators/multiverse/templates/record.rb @@ -0,0 +1,4 @@ +class <%= name.camelize %>Record < ActiveRecord::Base + self.abstract_class = true + establish_connection :"<%= name.underscore %>_#{Rails.env}" +end diff --git a/lib/multiverse.rb b/lib/multiverse.rb new file mode 100644 index 0000000..7cc132f --- /dev/null +++ b/lib/multiverse.rb @@ -0,0 +1,41 @@ +require "multiverse/generators" +require "multiverse/patches" +require "multiverse/railtie" +require "multiverse/version" + +module Multiverse + class << self + attr_writer :db + + def db + @db ||= ENV["DB"] + end + + def db_dir + db_dir = db ? "db/#{db}" : "db" + abort "Unknown DB: #{db}" if db && !Dir.exist?(db_dir) + db_dir + end + + def parent_class_name + db ? "#{db.camelize}Record" : "ApplicationRecord" + end + + def record_class + record_class = parent_class_name.safe_constantize + abort "Missing model: #{parent_class_name}" unless record_class + record_class + end + + def migrate_path + "#{db_dir}/migrate" + end + end +end + +ActiveSupport.on_load(:active_record) do + ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend Multiverse::DatabaseTasks + ActiveRecord::Migration.prepend Multiverse::Migration + ActiveRecord::SchemaMigration.singleton_class.prepend Multiverse::SchemaMigration + ActiveRecord::SchemaDumper.singleton_class.prepend Multiverse::SchemaDumper +end diff --git a/lib/multiverse/generators.rb b/lib/multiverse/generators.rb new file mode 100644 index 0000000..f18e871 --- /dev/null +++ b/lib/multiverse/generators.rb @@ -0,0 +1,15 @@ +module Multiverse + module Generators + module ModelGenerator + def parent_class_name + Multiverse.parent_class_name + end + end + + module Migration + def db_migrate_path + Multiverse.migrate_path + end + end + end +end diff --git a/lib/multiverse/patches.rb b/lib/multiverse/patches.rb new file mode 100644 index 0000000..0f06a25 --- /dev/null +++ b/lib/multiverse/patches.rb @@ -0,0 +1,46 @@ +module Multiverse + module DatabaseTasks + def each_current_configuration(environment) + if Multiverse.db + environments = ["#{Multiverse.db}_#{environment}"] + environments << "#{Multiverse.db}_test" if environment == "development" + self.migrations_paths = Multiverse.migrate_path + self.db_dir = Multiverse.db_dir + else + environments = [environment] + environments << "test" if environment == "development" + end + + configurations = ActiveRecord::Base.configurations.values_at(*environments) + configurations.compact.each do |configuration| + yield configuration unless configuration['database'].blank? + end + end + end + + module Migration + def connection + @connection || Multiverse.record_class.connection + end + + # TODO don't checkout main connection at all + def exec_migration(_, direction) + Multiverse.record_class.connection_pool.with_connection do |conn| + super(conn, direction) + end + end + end + + module SchemaMigration + def connection + Multiverse.record_class.connection + end + end + + module SchemaDumper + def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) + new(Multiverse.record_class.connection, generate_options(Multiverse.record_class)).dump(stream) + stream + end + end +end diff --git a/lib/multiverse/railtie.rb b/lib/multiverse/railtie.rb new file mode 100644 index 0000000..f3401b2 --- /dev/null +++ b/lib/multiverse/railtie.rb @@ -0,0 +1,22 @@ +require "rails/railtie" + +module Multiverse + class Railtie < Rails::Railtie + generators do + require "rails/generators/active_record/migration" + ActiveRecord::Generators::Migration.prepend(Multiverse::Generators::Migration) + + require "rails/generators/active_record/model/model_generator" + ActiveRecord::Generators::ModelGenerator.prepend(Multiverse::Generators::ModelGenerator) + end + + rake_tasks do + namespace :db do + task :load_config do + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = [Multiverse.migrate_path] + ActiveRecord::Tasks::DatabaseTasks.db_dir = [Multiverse.db_dir] + end + end + end + end +end diff --git a/lib/multiverse/version.rb b/lib/multiverse/version.rb new file mode 100644 index 0000000..49422b5 --- /dev/null +++ b/lib/multiverse/version.rb @@ -0,0 +1,3 @@ +module Multiverse + VERSION = "0.0.1" +end diff --git a/multiverse.gemspec b/multiverse.gemspec new file mode 100644 index 0000000..2fa8f56 --- /dev/null +++ b/multiverse.gemspec @@ -0,0 +1,27 @@ + +lib = File.expand_path("../lib", __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require "multiverse/version" + +Gem::Specification.new do |spec| + spec.name = "multiverse" + spec.version = Multiverse::VERSION + spec.authors = ["Andrew Kane"] + spec.email = ["andrew@chartkick.com"] + + spec.summary = "Multiple databases for Rails" + spec.homepage = "https://github.com/ankane/multiverse" + + spec.files = `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{^(test|spec|features)/}) + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "rails", ">= 5" + + spec.add_development_dependency "bundler" + spec.add_development_dependency "rake" + spec.add_development_dependency "minitest" +end diff --git a/test/multiverse_test.rb b/test/multiverse_test.rb new file mode 100644 index 0000000..94265d2 --- /dev/null +++ b/test/multiverse_test.rb @@ -0,0 +1,7 @@ +require_relative "test_helper" + +class MultiverseTest < Minitest::Test + def test_that_it_has_a_version_number + refute_nil ::Multiverse::VERSION + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..0f9036d --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,4 @@ +require "bundler/setup" +Bundler.require(:default) +require "minitest/autorun" +require "minitest/pride"