Skip to content

Commit

Permalink
Rewrite adpaters to support flexible switching
Browse files Browse the repository at this point in the history
This is a complete rewrite of the adapters & of the test suite.

- Test suite rewritten in minitest
- Adapters handle separate physical tenant hosts seamlessly
- Supports Rails 5.1 and above only
- Dropped all adapters other than PG and MySQL (didn't get time, will
  accept PRs)
- Added 'tenant resolvers' for generating full database
  configurations from tenant names
- Added `tenant_decorator` option for using a proc to decorate your
  tenant database/schema names with Rails.env or anything else
- All excluded models share a connection pool
- All adapters & switching strategies are threadsafe
  • Loading branch information
Mike Campbell committed Jun 26, 2017
1 parent 194b289 commit 6431e58
Show file tree
Hide file tree
Showing 149 changed files with 1,081 additions and 3,376 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ pkg/*
*.log
.idea
*.sw[pno]
spec/config/database.yml
spec/config/databases.yml
spec/dummy/config/database.yml
cookbooks
tmp
spec/dummy/db/*.sqlite3
.DS_Store
test/debug.log
4 changes: 0 additions & 4 deletions .rspec

This file was deleted.

1 change: 0 additions & 1 deletion .ruby-gemset

This file was deleted.

1 change: 0 additions & 1 deletion .ruby-version

This file was deleted.

12 changes: 1 addition & 11 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
language: ruby
rvm:
- 2.0.0
- 2.1.9
- 2.2.4
- 2.3.1
- jruby-9.0.5.0
gemfile:
- gemfiles/rails_4_0.gemfile
- gemfiles/rails_4_1.gemfile
- gemfiles/rails_4_2.gemfile
- gemfiles/rails_5_0.gemfile
- gemfiles/rails_5_1.gemfile
bundler_args: --without local
before_install:
- gem install bundler -v '> 1.5.0'
env:
RUBY_GC_MALLOC_LIMIT: 90000000
RUBY_FREE_MIN: 200000
matrix:
exclude:
- rvm: 2.0.0
gemfile: gemfiles/rails_5_0.gemfile
- rvm: 2.1.9
gemfile: gemfiles/rails_5_0.gemfile
fast_finish: true
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ source 'http://rubygems.org'

gemspec

gem 'rails', '>= 3.1.2'
gem 'rails', '>= 5.1.0'

group :local do
gem 'pry'
Expand Down
51 changes: 23 additions & 28 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
require 'bundler' rescue 'You must `gem install bundler` and `bundle install` to run rake tasks'
Bundler.setup
Bundler::GemHelper.install_tasks

require 'appraisal'

require "rspec"
require "rspec/core/rake_task"

RSpec::Core::RakeTask.new(:spec => %w{ db:copy_credentials db:test:prepare }) do |spec|
spec.pattern = "spec/**/*_spec.rb"
# spec.rspec_opts = '--order rand:16996'
require "rake/testtask"
require 'yaml'

Rake::TestTask.new do |t|
t.libs = ["lib"]
t.warning = false
# t.verbose = true
t.test_files = FileList['test/*_test.rb']
end

namespace :spec do
[:tasks, :unit, :adapters, :integration].each do |type|
RSpec::Core::RakeTask.new(type => :spec) do |spec|
spec.pattern = "spec/#{type}/**/*_spec.rb"
end
end
end
require 'appraisal'
# require "#{File.join(File.dirname(__FILE__), 'test', 'test_helper')}"

task :console do
require 'pry'
Expand All @@ -27,18 +21,18 @@ task :console do
Pry.start
end

task :default => :spec
task default: :test

namespace :db do
namespace :test do
task :prepare => %w{postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db}
task prepare: %w{postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db}
end

desc "copy sample database credential files over if real files don't exist"
task :copy_credentials do
require 'fileutils'
apartment_db_file = 'spec/config/database.yml'
rails_db_file = 'spec/dummy/config/database.yml'
apartment_db_file = 'test/databases.yml'
rails_db_file = 'test/dummy/config/database.yml'

FileUtils.copy(apartment_db_file + '.sample', apartment_db_file, :verbose => true) unless File.exists?(apartment_db_file)
FileUtils.copy(rails_db_file + '.sample', rails_db_file, :verbose => true) unless File.exists?(rails_db_file)
Expand All @@ -47,13 +41,14 @@ end

namespace :postgres do
require 'active_record'
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"

desc 'Build the PostgreSQL test databases'
task :build_db do
%x{ createdb -E UTF8 #{pg_config['database']} -U#{pg_config['username']} } rescue "test db already exists"
ActiveRecord::Base.establish_connection pg_config
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
ActiveRecord::Migration.suppress_messages do
load(File.join(File.dirname(__FILE__), "test/dummy/db/schema.rb"))
end
end

desc "drop the PostgreSQL test database"
Expand All @@ -66,26 +61,26 @@ end

namespace :mysql do
require 'active_record'
require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"

desc 'Build the MySQL test databases'
task :build_db do
%x{ mysqladmin -u #{my_config['username']} --password=#{my_config['password']} create #{my_config['database']} } rescue "test db already exists"
%x{ /usr/local/mysql/bin/mysqladmin -u #{my_config['username']} --password=#{my_config['password']} create #{my_config['database']} } rescue "test db already exists"
ActiveRecord::Base.establish_connection my_config
ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
ActiveRecord::Migration.suppress_messages do
load(File.join(File.dirname(__FILE__), "test/dummy/db/schema.rb"))
end
end

desc "drop the MySQL test database"
task :drop_db do
puts "dropping database #{my_config['database']}"
%x{ mysqladmin -u #{my_config['username']} --password=#{my_config['password']} drop #{my_config['database']} --force}
%x{ /usr/local/mysql/bin/mysqladmin -u #{my_config['username']} --password=#{my_config['password']} drop #{my_config['database']} --force}
end

end

# TODO clean this up
def config
Apartment::Test.config['connections']
@config ||= YAML.load(ERB.new(IO.read('test/databases.yml')).result)['connections']
end

def pg_config
Expand Down
5 changes: 1 addition & 4 deletions apartment.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,12 @@ Gem::Specification.new do |s|
s.homepage = %q{https://github.com/influitive/apartment}
s.licenses = ["MIT"]

# must be >= 3.1.2 due to bug in prepared_statements
s.add_dependency 'activerecord', '>= 3.1.2', '< 6.0'
s.add_dependency 'activerecord', '>= 5.1.0'
s.add_dependency 'rack', '>= 1.3.6'
s.add_dependency 'public_suffix', '~> 2.0.5'

s.add_development_dependency 'appraisal'
s.add_development_dependency 'rake', '~> 0.9'
s.add_development_dependency 'rspec', '~> 3.4'
s.add_development_dependency 'rspec-rails', '~> 3.4'
s.add_development_dependency 'capybara', '~> 2.0'

if defined?(JRUBY_VERSION)
Expand Down
12 changes: 0 additions & 12 deletions gemfiles/rails_4_0.gemfile

This file was deleted.

12 changes: 0 additions & 12 deletions gemfiles/rails_4_2.gemfile

This file was deleted.

12 changes: 0 additions & 12 deletions gemfiles/rails_5_0.gemfile

This file was deleted.

2 changes: 1 addition & 1 deletion gemfiles/rails_4_1.gemfile → gemfiles/rails_5_1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

source "http://rubygems.org"

gem "rails", "~> 4.1.0"
gem "rails", "~> 5.1.0"

group :local do
gem "pry"
Expand Down
55 changes: 26 additions & 29 deletions lib/apartment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,47 @@
require 'apartment/tenant'

module Apartment

class << self

extend Forwardable

ACCESSOR_METHODS = [:use_schemas, :use_sql, :seed_after_create, :prepend_environment, :append_environment, :with_multi_server_setup ]
WRITER_METHODS = [:tenant_names, :database_schema_file, :excluded_models, :default_schema, :persistent_schemas, :connection_class, :tld_length, :db_migrate_tenants, :seed_data_file]
ACCESSOR_METHODS = [
:use_sql, :seed_after_create, :tenant_decorator,
:force_reconnect_on_switch
]
WRITER_METHODS = [
:tenant_names, :database_schema_file, :excluded_models,
:persistent_schemas, :connection_class, :tld_length, :db_migrate_tenants,
:seed_data_file, :default_tenant
]
OTHER_METHODS = [:tenant_resolver, :resolver_class]

attr_accessor(*ACCESSOR_METHODS)
attr_writer(*WRITER_METHODS)

def_delegators :connection_class, :connection, :connection_config, :establish_connection
def_delegators :connection_class, :connection, :connection_config,
:establish_connection, :connection_handler

# configure apartment with available options
def configure
yield self if block_given?
end

def tenant_resolver
@tenant_resolver ||= @resolver_class.new(connection_config)
end

def tenant_resolver=(resolver_class)
remove_instance_variable(:@tenant_resolver) if instance_variable_defined?(:@tenant_resolver)
@resolver_class = resolver_class
end

def tenant_names
extract_tenant_config.keys.map(&:to_s)
@tenant_names.respond_to?(:call) ? @tenant_names.call : (@tenant_names || [])
end

def tenants_with_config
extract_tenant_config
end

def db_config_for(tenant)
(tenants_with_config[tenant] || connection_config).with_indifferent_access
end

# Whether or not db:migrate should also migrate tenants
# defaults to true
def db_migrate_tenants
Expand All @@ -48,11 +59,9 @@ def excluded_models
@excluded_models || []
end

def default_schema
@default_schema || "public" # TODO 'public' is postgres specific
def default_tenant
@default_tenant || tenant_resolver.init_config
end
alias :default_tenant :default_schema
alias :default_tenant= :default_schema=

def persistent_schemas
@persistent_schemas || []
Expand All @@ -74,22 +83,10 @@ def seed_data_file
@seed_data_file = "#{Rails.root}/db/seeds.rb"
end

# Reset all the config for Apartment
def reset
(ACCESSOR_METHODS + WRITER_METHODS).each{|method| remove_instance_variable(:"@#{method}") if instance_variable_defined?(:"@#{method}") }
end

def extract_tenant_config
return {} unless @tenant_names
values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
unless values.is_a? Hash
values = values.each_with_object({}) do |tenant, hash|
hash[tenant] = connection_config
end
(ACCESSOR_METHODS + WRITER_METHODS + OTHER_METHODS).each do |method|
remove_instance_variable(:"@#{method}") if instance_variable_defined?(:"@#{method}")
end
values.with_indifferent_access
rescue ActiveRecord::StatementInvalid
{}
end
end

Expand Down
Loading

0 comments on commit 6431e58

Please sign in to comment.