├── spec
├── dummy
│ ├── public
│ │ ├── favicon.ico
│ │ ├── stylesheets
│ │ │ └── .gitkeep
│ │ ├── 422.html
│ │ ├── 404.html
│ │ └── 500.html
│ ├── app
│ │ ├── views
│ │ │ ├── application
│ │ │ │ └── index.html.erb
│ │ │ └── layouts
│ │ │ │ └── application.html.erb
│ │ ├── helpers
│ │ │ └── application_helper.rb
│ │ ├── models
│ │ │ ├── company.rb
│ │ │ └── user.rb
│ │ └── controllers
│ │ │ └── application_controller.rb
│ ├── lib
│ │ └── fake_dj_class.rb
│ ├── config
│ │ ├── routes.rb
│ │ ├── environment.rb
│ │ ├── initializers
│ │ │ ├── apartment.rb
│ │ │ ├── mime_types.rb
│ │ │ ├── inflections.rb
│ │ │ ├── backtrace_silencers.rb
│ │ │ ├── session_store.rb
│ │ │ └── secret_token.rb
│ │ ├── locales
│ │ │ └── en.yml
│ │ ├── boot.rb
│ │ ├── database.yml.sample
│ │ ├── environments
│ │ │ ├── development.rb
│ │ │ ├── test.rb
│ │ │ └── production.rb
│ │ └── application.rb
│ ├── db
│ │ ├── test.sqlite3
│ │ ├── seeds.rb
│ │ ├── migrate
│ │ │ ├── 20111202022214_create_table_books.rb
│ │ │ └── 20110613152810_create_dummy_models.rb
│ │ └── schema.rb
│ ├── config.ru
│ ├── Rakefile
│ └── script
│ │ └── rails
├── support
│ ├── config.rb
│ ├── capybara_sessions.rb
│ ├── apartment_helpers.rb
│ ├── requirements.rb
│ └── contexts.rb
├── apartment_spec.rb
├── integration
│ ├── middleware
│ │ ├── domain_elevator_spec.rb
│ │ ├── subdomain_elevator_spec.rb
│ │ └── generic_elevator_spec.rb
│ ├── apartment_rake_integration_spec.rb
│ └── delayed_job_integration_spec.rb
├── config
│ └── database.yml.sample
├── unit
│ ├── reloader_spec.rb
│ ├── middleware
│ │ ├── subdomain_elevator_spec.rb
│ │ └── domain_elevator_spec.rb
│ ├── config_spec.rb
│ └── migrator_spec.rb
├── adapters
│ ├── mysql2_adapter_spec.rb
│ └── postgresql_adapter_spec.rb
├── examples
│ ├── elevator_examples.rb
│ ├── db_adapter_examples.rb
│ ├── generic_adapter_examples.rb
│ └── schema_adapter_examples.rb
├── spec_helper.rb
├── tasks
│ └── apartment_rake_spec.rb
└── database_spec.rb
├── .rspec
├── .rvmrc
├── Gemfile
├── .travis.yml
├── lib
├── apartment
│ ├── version.rb
│ ├── sidekiq
│ │ ├── client
│ │ │ └── database_middleware.rb
│ │ └── server
│ │ │ └── database_middleware.rb
│ ├── elevators
│ │ ├── subdomain.rb
│ │ ├── domain.rb
│ │ └── generic.rb
│ ├── console.rb
│ ├── delayed_job
│ │ ├── enqueue.rb
│ │ ├── requirements.rb
│ │ ├── hooks.rb
│ │ └── active_record.rb
│ ├── adapters
│ │ ├── mysql2_adapter.rb
│ │ ├── postgresql_adapter.rb
│ │ └── abstract_adapter.rb
│ ├── reloader.rb
│ ├── migrator.rb
│ ├── database.rb
│ └── railtie.rb
├── tasks
│ └── apartment.rake
└── apartment.rb
├── README.md
├── .pryrc
├── .gitignore
├── circle.yml
├── apartment.gemspec
├── Rakefile
└── HISTORY.md
/spec/dummy/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/spec/dummy/public/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --colour
2 | --format documentation
3 |
--------------------------------------------------------------------------------
/.rvmrc:
--------------------------------------------------------------------------------
1 | rvm --create ruby-1.9.3@apartment
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | gemspec
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/application/index.html.erb:
--------------------------------------------------------------------------------
1 |
Index!!
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.9.2
4 | - 1.9.3
5 |
--------------------------------------------------------------------------------
/lib/apartment/version.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | VERSION = "0.16.0"
3 | end
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Apartment now lives at https://github.com/influitive/apartment
2 |
--------------------------------------------------------------------------------
/spec/dummy/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/.pryrc:
--------------------------------------------------------------------------------
1 | if defined?(Rails) && Rails.env
2 | extend Rails::ConsoleMethods
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/company.rb:
--------------------------------------------------------------------------------
1 | class Company < ActiveRecord::Base
2 | # Dummy models
3 | end
--------------------------------------------------------------------------------
/spec/dummy/lib/fake_dj_class.rb:
--------------------------------------------------------------------------------
1 | class FakeDjClass
2 |
3 | def perform
4 | end
5 |
6 | end
--------------------------------------------------------------------------------
/spec/dummy/config/routes.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.routes.draw do
2 | root :to => 'application#index'
3 | end
4 |
--------------------------------------------------------------------------------
/spec/dummy/db/test.sqlite3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bradrobertson/apartment/HEAD/spec/dummy/db/test.sqlite3
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | pkg/*
5 | *.log
6 | *.sw[pno]
7 | spec/config/database.yml
8 | spec/dummy/config/database.yml
9 |
--------------------------------------------------------------------------------
/spec/dummy/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | include Apartment::Delayed::Job::Hooks
3 | def perform; end
4 | # Dummy models
5 | end
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | ruby:
3 | version: 1.9.3-p194
4 |
5 | test:
6 | override:
7 | - bundle exec rake spec:
8 | environment:
9 | RAILS_ENV: test
--------------------------------------------------------------------------------
/spec/dummy/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery
3 |
4 | def index
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/spec/dummy/db/seeds.rb:
--------------------------------------------------------------------------------
1 | def create_users
2 | 3.times do |x|
3 | user = User.find_or_initialize_by_name "Some User #{x}"
4 | user.save
5 | end
6 | end
7 |
8 | create_users
--------------------------------------------------------------------------------
/spec/dummy/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Dummy::Application
5 |
--------------------------------------------------------------------------------
/spec/dummy/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the rails application
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the rails application
5 | Dummy::Application.initialize!
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/apartment.rb:
--------------------------------------------------------------------------------
1 | Apartment.configure do |config|
2 | config.excluded_models = ["Company"]
3 | config.database_names = lambda{ Company.scoped.collect(&:database) }
4 | end
--------------------------------------------------------------------------------
/spec/support/config.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 |
3 | module Test
4 |
5 | def self.config
6 | @config ||= YAML.load_file('spec/config/database.yml')
7 | end
8 |
9 | end
10 |
11 | end
--------------------------------------------------------------------------------
/spec/dummy/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Sample localization file for English. Add more files in this directory for other locales.
2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3 |
4 | en:
5 | hello: "Hello world"
6 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 | # Mime::Type.register_alias "text/html", :iphone
6 |
--------------------------------------------------------------------------------
/spec/apartment_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment do
4 | it "should be valid" do
5 | Apartment.should be_a(Module)
6 | end
7 |
8 | it "should be a valid app" do
9 | ::Rails.application.should be_a(Dummy::Application)
10 | end
11 | end
--------------------------------------------------------------------------------
/spec/dummy/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 | require 'rake'
6 |
7 | Dummy::Application.load_tasks
8 |
--------------------------------------------------------------------------------
/spec/dummy/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Dummy
5 | <%= stylesheet_link_tag :all %>
6 | <%= javascript_include_tag :defaults %>
7 | <%= csrf_meta_tag %>
8 |
9 |
10 |
11 | <%= yield %>
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/spec/integration/middleware/domain_elevator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Elevators::Domain, :elevator => true do
4 |
5 | let(:domain1) { "http://#{database1}.com" }
6 | let(:domain2) { "http://#{database2}.com" }
7 |
8 | it_should_behave_like "an apartment elevator"
9 | end
10 |
--------------------------------------------------------------------------------
/spec/integration/middleware/subdomain_elevator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Elevators::Subdomain, :elevator => true do
4 |
5 | let(:domain1) { "http://#{database1}.example.com" }
6 | let(:domain2) { "http://#{database2}.example.com" }
7 |
8 | it_should_behave_like "an apartment elevator"
9 | end
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20111202022214_create_table_books.rb:
--------------------------------------------------------------------------------
1 | class CreateTableBooks < ActiveRecord::Migration
2 | def up
3 | create_table :books do |t|
4 | t.string :name
5 | t.integer :pages
6 | t.datetime :published
7 | end
8 | end
9 |
10 | def down
11 | drop_table :books
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/spec/dummy/script/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3 |
4 | APP_PATH = File.expand_path('../../config/application', __FILE__)
5 | require File.expand_path('../../config/boot', __FILE__)
6 | require 'rails/commands'
7 |
--------------------------------------------------------------------------------
/lib/apartment/sidekiq/client/database_middleware.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Sidekiq
3 | module Client
4 | class DatabaseMiddleware
5 | def call(worker_class, item, queue)
6 | item["apartment"] = Apartment::Database.current_database
7 | yield
8 | end
9 | end
10 | end
11 | end
12 | end
--------------------------------------------------------------------------------
/lib/apartment/sidekiq/server/database_middleware.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Sidekiq
3 | module Server
4 | class DatabaseMiddleware
5 | def call(worker_class, item, queue)
6 | Apartment::Database.process(item['apartment']) do
7 | yield
8 | end
9 | end
10 | end
11 | end
12 | end
13 | end
--------------------------------------------------------------------------------
/spec/dummy/config/boot.rb:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 |
3 | require 'yaml'
4 | YAML::ENGINE.yamler = 'syck'
5 |
6 | gemfile = File.expand_path('../../../../Gemfile', __FILE__)
7 |
8 | if File.exist?(gemfile)
9 | ENV['BUNDLE_GEMFILE'] = gemfile
10 | require 'bundler'
11 | Bundler.setup
12 | end
13 |
14 | $:.unshift File.expand_path('../../../../lib', __FILE__)
--------------------------------------------------------------------------------
/spec/support/capybara_sessions.rb:
--------------------------------------------------------------------------------
1 | module RSpec
2 | module Integration
3 | module CapybaraSessions
4 |
5 | def in_new_session(&block)
6 | yield new_session
7 | end
8 |
9 | def new_session
10 | Capybara::Session.new(Capybara.current_driver, Capybara.app)
11 | end
12 |
13 | end
14 | end
15 | end
--------------------------------------------------------------------------------
/spec/config/database.yml.sample:
--------------------------------------------------------------------------------
1 | connections:
2 | postgresql:
3 | adapter: postgresql
4 | database: apartment_postgresql_test
5 | min_messages: WARNING
6 | username: postgres
7 | schema_search_path: public
8 | password:
9 |
10 | mysql:
11 | adapter: mysql2
12 | database: apartment_mysql_test
13 | username: root
14 | password:
15 |
--------------------------------------------------------------------------------
/lib/apartment/elevators/subdomain.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Elevators
3 | # Provides a rack based db switching solution based on subdomains
4 | # Assumes that database name should match subdomain
5 | #
6 | class Subdomain < Generic
7 |
8 | def parse_database_name(request)
9 | request.subdomain.present? && request.subdomain || nil
10 | end
11 | end
12 | end
13 | end
--------------------------------------------------------------------------------
/spec/integration/middleware/generic_elevator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Elevators::Generic, :elevator => true do
4 |
5 | # NOTE, see spec/dummy/config/application.rb to see the Proc that defines the behaviour here
6 | let(:domain1) { "http://#{database1}.com?db=#{database1}" }
7 | let(:domain2) { "http://#{database2}.com?db=#{database2}" }
8 |
9 | it_should_behave_like "an apartment elevator"
10 | end
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format
4 | # (all these examples are active by default):
5 | # ActiveSupport::Inflector.inflections do |inflect|
6 | # inflect.plural /^(ox)$/i, '\1en'
7 | # inflect.singular /^(ox)en/i, '\1'
8 | # inflect.irregular 'person', 'people'
9 | # inflect.uncountable %w( fish sheep )
10 | # end
11 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Dummy::Application.config.session_store :cookie_store, :key => '_dummy_session'
4 |
5 | # Use the database for sessions instead of the cookie-based default,
6 | # which shouldn't be used to store highly confidential information
7 | # (create the session table with "rails generate session_migration")
8 | # Dummy::Application.config.session_store :active_record_store
9 |
--------------------------------------------------------------------------------
/lib/apartment/console.rb:
--------------------------------------------------------------------------------
1 | # A workaraound to get `reload!` to also call Apartment::Database.init
2 | # This is unfortunate, but I haven't figured out how to hook into the reload process *after* files are reloaded
3 |
4 | # reloads the environment
5 | def reload!(print=true)
6 | puts "Reloading..." if print
7 | # This triggers the to_prepare callbacks
8 | ActionDispatch::Callbacks.new(Proc.new {}).call({})
9 | # Manually init Apartment again once classes are reloaded
10 | Apartment::Database.init
11 | true
12 | end
13 |
--------------------------------------------------------------------------------
/spec/dummy/config/database.yml.sample:
--------------------------------------------------------------------------------
1 | # Warning: The database defined as "test" will be erased and
2 | # re-generated from your development database when you run "rake".
3 | # Do not set this db to the same as development or production.
4 | test:
5 | adapter: postgresql
6 | database: apartment_postgresql_test
7 | min_messages: WARNING
8 | pool: 5
9 | timeout: 5000
10 |
11 | development:
12 | adapter: postgresql
13 | database: apartment_postgresql_development
14 | min_messages: WARNING
15 | pool: 5
16 | timeout: 5000
17 |
--------------------------------------------------------------------------------
/spec/dummy/config/initializers/secret_token.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 | # Make sure the secret is at least 30 characters and all random,
6 | # no regular words or you'll be exposed to dictionary attacks.
7 | Dummy::Application.config.secret_token = '7d33999a86884f74c897c98ecca4277090b69e9f23df8d74bcadd57435320a7a16de67966f9b69d62e7d5ec553bd2febbe64c721e05bc1bc1e82c7a7d2395201'
8 |
--------------------------------------------------------------------------------
/lib/apartment/elevators/domain.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Elevators
3 | # Provides a rack based db switching solution based on domain
4 | # Assumes that database name should match domain
5 | # Parses request host for second level domain
6 | # eg. example.com => example
7 | # www.example.bc.ca => example
8 | #
9 | class Domain < Generic
10 |
11 | def parse_database_name(request)
12 | return nil if request.host.blank?
13 |
14 | request.host.match(/(www.)?(?[^.]*)/)["sld"]
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/spec/unit/reloader_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Reloader do
4 |
5 | context "using postgresql schemas" do
6 |
7 | before do
8 | Apartment.excluded_models = ["Company"]
9 | Company.reset_table_name # ensure we're clean
10 | end
11 |
12 | subject{ Apartment::Reloader.new(mock("Rack::Application", :call => nil)) }
13 |
14 | it "should initialize apartment when called" do
15 | Company.table_name.should_not include('public.')
16 | subject.call(mock('env'))
17 | Company.table_name.should include('public.')
18 | end
19 | end
20 |
21 |
22 | end
--------------------------------------------------------------------------------
/lib/apartment/delayed_job/enqueue.rb:
--------------------------------------------------------------------------------
1 | require 'delayed_job'
2 | require 'apartment/delayed_job/active_record' # ensure that our AR hooks are loaded when queueing
3 |
4 | module Apartment
5 | module Delayed
6 | module Job
7 |
8 | # Will enqueue a job ensuring that it happens within the main 'public' database
9 | #
10 | # Note that this should not longer be required for versions >= 0.11.0 when using postgresql schemas
11 | #
12 | def self.enqueue(payload_object, options = {})
13 | Apartment::Database.process do
14 | ::Delayed::Job.enqueue(payload_object, options)
15 | end
16 | end
17 |
18 | end
19 | end
20 | end
--------------------------------------------------------------------------------
/spec/adapters/mysql2_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'apartment/adapters/mysql2_adapter'
3 |
4 | describe Apartment::Adapters::Mysql2Adapter do
5 |
6 | let(:config){ Apartment::Test.config['connections']['mysql'] }
7 | subject{ Apartment::Database.mysql2_adapter config.symbolize_keys }
8 |
9 | def database_names
10 | ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect{|row| row[0]}
11 | end
12 |
13 | let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
14 |
15 | it_should_behave_like "a generic apartment adapter"
16 | it_should_behave_like "a db based apartment adapter"
17 | end
18 |
--------------------------------------------------------------------------------
/lib/apartment/elevators/generic.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Elevators
3 | # Provides a rack based db switching solution based on request
4 | #
5 | class Generic
6 |
7 | def initialize(app, processor = nil)
8 | @app = app
9 | @processor = processor || method(:parse_database_name)
10 | end
11 |
12 | def call(env)
13 | request = ActionDispatch::Request.new(env)
14 |
15 | database = @processor.call(request)
16 |
17 | Apartment::Database.switch database if database
18 |
19 | @app.call(env)
20 | end
21 |
22 | def parse_database_name(request)
23 | raise "Override"
24 | end
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/spec/unit/middleware/subdomain_elevator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Elevators::Subdomain do
4 |
5 | describe "#parse_database_name" do
6 | it "should parse subdomain" do
7 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.com')
8 | elevator = Apartment::Elevators::Subdomain.new(nil)
9 | elevator.parse_database_name(request).should == 'foo'
10 | end
11 |
12 | it "should return nil when no subdomain" do
13 | request = ActionDispatch::Request.new('HTTP_HOST' => 'bar.com')
14 | elevator = Apartment::Elevators::Subdomain.new(nil)
15 | elevator.parse_database_name(request).should be_nil
16 | end
17 |
18 | end
19 |
20 | end
--------------------------------------------------------------------------------
/lib/apartment/adapters/mysql2_adapter.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 |
3 | module Database
4 |
5 | def self.mysql2_adapter(config)
6 | Adapters::Mysql2Adapter.new config
7 | end
8 | end
9 |
10 | module Adapters
11 |
12 | class Mysql2Adapter < AbstractAdapter
13 |
14 | protected
15 |
16 | # Connect to new database
17 | # Abstract adapter will catch generic ActiveRecord error
18 | # Catch specific adapter errors here
19 | #
20 | # @param {String} database Database name
21 | #
22 | def connect_to_new(database)
23 | super
24 | rescue Mysql2::Error
25 | raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/apartment/delayed_job/requirements.rb:
--------------------------------------------------------------------------------
1 | require 'apartment/delayed_job/active_record' # ensure that our AR hooks are loaded when queueing
2 |
3 | module Apartment
4 | module Delayed
5 |
6 | # Mix this module into any ActiveRecord model that gets serialized by DJ
7 | module Requirements
8 | attr_accessor :database
9 |
10 | def self.included(klass)
11 | klass.after_find :set_database # set db when records are pulled so they deserialize properly
12 | klass.before_save :set_database # set db before records are saved so that they also get deserialized properly
13 | end
14 |
15 | private
16 |
17 | def set_database
18 | @database = Apartment::Database.current_database
19 | end
20 |
21 | end
22 | end
23 | end
--------------------------------------------------------------------------------
/lib/apartment/reloader.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 |
3 | class Reloader
4 |
5 | # Middleware used in development to init Apartment for each request
6 | # Necessary due to code reload (annoying). When models are reloaded, they no longer have the proper table_name
7 | # That is prepended with the schema (if using postgresql schemas)
8 | # I couldn't figure out how to properly hook into the Rails reload process *after* files are reloaded
9 | # so I've used this in the meantime.
10 | #
11 | # Also see apartment/console for the re-definition of reload! that re-init's Apartment
12 | #
13 | def initialize(app)
14 | @app = app
15 | end
16 |
17 | def call(env)
18 | Database.init
19 | @app.call(env)
20 | end
21 |
22 | end
23 |
24 | end
--------------------------------------------------------------------------------
/spec/dummy/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The change you wanted was rejected.
23 |
Maybe you tried to change something you didn't have access to.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/dummy/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
The page you were looking for doesn't exist.
23 |
You may have mistyped the address or the page may have moved.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/spec/dummy/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
17 |
18 |
19 |
20 |
21 |
22 |
We're sorry, but something went wrong.
23 |
We've been notified about this issue and we'll take a look at it shortly.
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/apartment/delayed_job/hooks.rb:
--------------------------------------------------------------------------------
1 | require 'apartment/delayed_job/enqueue'
2 |
3 | module Apartment
4 | module Delayed
5 | module Job
6 |
7 | # Before and after hooks for performing Delayed Jobs within a particular apartment database
8 | # Include these in your delayed jobs models and make sure provide a @database attr that will be serialized by DJ
9 | # Note also that any models that are being serialized need the Apartment::Delayed::Requirements module mixed in to it
10 | module Hooks
11 |
12 | attr_accessor :database
13 |
14 | def before(job)
15 | @_current_database = Apartment::Database.current_database
16 | Apartment::Database.switch(job.payload_object.database) if job.payload_object.database
17 | end
18 |
19 | def after
20 | Apartment::Database.switch(@_current_database)
21 | end
22 |
23 | end
24 | end
25 | end
26 | end
--------------------------------------------------------------------------------
/lib/apartment/migrator.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 |
3 | module Migrator
4 |
5 | extend self
6 |
7 | # Migrate to latest
8 | def migrate(database)
9 | Database.process(database) do
10 | ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
11 | ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
12 | end
13 | end
14 | end
15 |
16 | # Migrate up/down to a specific version
17 | def run(direction, database, version)
18 | Database.process(database){ ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_path, version) }
19 | end
20 |
21 | # rollback latest migration `step` number of times
22 | def rollback(database, step = 1)
23 | Database.process(database){ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_path, step) }
24 | end
25 | end
26 |
27 | end
28 |
--------------------------------------------------------------------------------
/spec/unit/middleware/domain_elevator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Elevators::Domain do
4 |
5 | describe "#parse_database_name" do
6 | it "parses the host for a domain name" do
7 | request = ActionDispatch::Request.new('HTTP_HOST' => 'example.com')
8 | elevator = Apartment::Elevators::Domain.new(nil)
9 | elevator.parse_database_name(request).should == 'example'
10 | end
11 |
12 | it "ignores a www prefix and domain suffix" do
13 | request = ActionDispatch::Request.new('HTTP_HOST' => 'www.example.bc.ca')
14 | elevator = Apartment::Elevators::Domain.new(nil)
15 | elevator.parse_database_name(request).should == 'example'
16 | end
17 |
18 | it "returns nil if there is no host" do
19 | request = ActionDispatch::Request.new('HTTP_HOST' => '')
20 | elevator = Apartment::Elevators::Domain.new(nil)
21 | elevator.parse_database_name(request).should be_nil
22 | end
23 |
24 | end
25 |
26 | end
27 |
--------------------------------------------------------------------------------
/spec/examples/elevator_examples.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | shared_examples_for "an apartment elevator" do
4 |
5 | context "single request" do
6 | it "should switch the db" do
7 | ActiveRecord::Base.connection.schema_search_path.should_not == database1
8 |
9 | visit(domain1)
10 | ActiveRecord::Base.connection.schema_search_path.should == database1
11 | end
12 | end
13 |
14 | context "simultaneous requests" do
15 |
16 | let!(:c1_user_count) { api.process(database1){ (2 + rand(2)).times{ User.create } } }
17 | let!(:c2_user_count) { api.process(database2){ (c1_user_count + 2).times{ User.create } } }
18 |
19 | it "should fetch the correct user count for each session based on the elevator processor" do
20 | visit(domain1)
21 |
22 | in_new_session do |session|
23 | session.visit(domain2)
24 | User.count.should == c2_user_count
25 | end
26 |
27 | visit(domain1)
28 | User.count.should == c1_user_count
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb:
--------------------------------------------------------------------------------
1 | class CreateDummyModels < ActiveRecord::Migration
2 | def self.up
3 | create_table :companies do |t|
4 | t.boolean :dummy
5 | t.string :database
6 | end
7 |
8 | create_table :users do |t|
9 | t.string :name
10 | t.datetime :birthdate
11 | t.string :sex
12 | end
13 |
14 | create_table :delayed_jobs do |t|
15 | t.integer :priority, :default => 0
16 | t.integer :attempts, :default => 0
17 | t.text :handler
18 | t.text :last_error
19 | t.datetime :run_at
20 | t.datetime :locked_at
21 | t.datetime :failed_at
22 | t.string :locked_by
23 | t.datetime :created_at
24 | t.datetime :updated_at
25 | t.string :queue
26 | end
27 |
28 | add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
29 |
30 | end
31 |
32 | def self.down
33 | drop_table :companies
34 | drop_table :users
35 | drop_table :delayed_jobs
36 | end
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/lib/apartment/delayed_job/active_record.rb:
--------------------------------------------------------------------------------
1 | module ActiveRecord
2 | class Base
3 |
4 | # Overriding Delayed Job's monkey_patch of ActiveRecord so that it works with Apartment
5 | yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
6 |
7 | def self.yaml_new(klass, tag, val)
8 | Apartment::Database.process(val['database']) do
9 | klass.find(val['attributes']['id'])
10 | end
11 | rescue ActiveRecord::RecordNotFound => e
12 | raise Delayed::DeserializationError, e.message
13 | end
14 |
15 | # Rails > 3.0 now uses encode_with to determine what to encode with yaml
16 | # @override to include database attribute
17 | def encode_with_with_database(coder)
18 | coder['database'] = @database if @database.present?
19 | encode_with_without_database(coder)
20 | end
21 | alias_method_chain :encode_with, :database
22 |
23 | # Remain backwards compatible with old yaml serialization
24 | def to_yaml_properties
25 | ['@attributes', '@database'] # add in database attribute for serialization
26 | end
27 |
28 | end
29 | end
--------------------------------------------------------------------------------
/spec/examples/db_adapter_examples.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | shared_examples_for "a db based apartment adapter" do
4 | include Apartment::Spec::AdapterRequirements
5 |
6 | let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
7 |
8 | describe "#init" do
9 |
10 | it "should process model exclusions" do
11 | Apartment.configure do |config|
12 | config.excluded_models = ["Company"]
13 | end
14 |
15 | Apartment::Database.init
16 |
17 | Company.connection.object_id.should_not == ActiveRecord::Base.connection.object_id
18 | end
19 | end
20 |
21 | describe "#drop" do
22 | it "should raise an error for unknown database" do
23 | expect {
24 | subject.drop 'unknown_database'
25 | }.to raise_error(Apartment::DatabaseNotFound)
26 | end
27 | end
28 |
29 | describe "#switch" do
30 | it "should raise an error if database is invalid" do
31 | expect {
32 | subject.switch 'unknown_database'
33 | }.to raise_error(Apartment::DatabaseNotFound)
34 | end
35 | end
36 | end
--------------------------------------------------------------------------------
/spec/dummy/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the webserver when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Log error messages when you accidentally call methods on nil.
10 | config.whiny_nils = true
11 |
12 | # Show full error reports and disable caching
13 | config.consider_all_requests_local = true
14 | config.action_view.debug_rjs = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Don't care if the mailer can't send
18 | config.action_mailer.raise_delivery_errors = false
19 |
20 | # Print deprecation notices to the Rails logger
21 | config.active_support.deprecation = :log
22 |
23 | # Only use best-standards-support built into browsers
24 | config.action_dispatch.best_standards_support = :builtin
25 | end
26 |
27 |
--------------------------------------------------------------------------------
/spec/support/apartment_helpers.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Test
3 |
4 | extend self
5 |
6 | def reset
7 | Apartment.excluded_models = nil
8 | Apartment.use_postgres_schemas = nil
9 | Apartment.seed_after_create = nil
10 | Apartment.default_schema = nil
11 | end
12 |
13 | def next_db
14 | @x ||= 0
15 | "db%d" % @x += 1
16 | end
17 |
18 | def drop_schema(schema)
19 | ActiveRecord::Base.connection.execute("DROP SCHEMA IF EXISTS #{schema} CASCADE") rescue true
20 | end
21 |
22 | # Use this if you don't want to import schema.rb etc... but need the postgres schema to exist
23 | # basically for speed purposes
24 | def create_schema(schema)
25 | ActiveRecord::Base.connection.execute("CREATE SCHEMA #{schema}")
26 | end
27 |
28 | def load_schema
29 | silence_stream(STDOUT){ load(Rails.root.join('db', 'schema.rb')) }
30 | end
31 |
32 | def migrate
33 | ActiveRecord::Migrator.migrate(Rails.root + ActiveRecord::Migrator.migrations_path)
34 | end
35 |
36 | def rollback
37 | ActiveRecord::Migrator.rollback(Rails.root + ActiveRecord::Migrator.migrations_path)
38 | end
39 |
40 | end
41 | end
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift(File.dirname(__FILE__))
2 |
3 | # Configure Rails Environment
4 | ENV["RAILS_ENV"] = "test"
5 |
6 | require File.expand_path("../dummy/config/environment.rb", __FILE__)
7 | require "rspec/rails"
8 | require 'capybara/rspec'
9 | require 'capybara/rails'
10 | require 'pry'
11 |
12 | silence_warnings{ IRB = Pry }
13 |
14 | ActionMailer::Base.delivery_method = :test
15 | ActionMailer::Base.perform_deliveries = true
16 | ActionMailer::Base.default_url_options[:host] = "test.com"
17 |
18 | Rails.backtrace_cleaner.remove_silencers!
19 |
20 | # Load support files
21 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
22 |
23 |
24 | RSpec.configure do |config|
25 |
26 | config.include RSpec::Integration::CapybaraSessions, :type => :request
27 |
28 | config.before(:all) do
29 | # Ensure that each test starts with a clean connection
30 | # Necessary as some tests will leak things like current_schema into the next test
31 | ActiveRecord::Base.clear_all_connections!
32 | end
33 |
34 | config.after(:each) do
35 | Apartment.reset
36 | end
37 |
38 | end
39 |
40 | # Load shared examples, must happen after configure for RSpec 3
41 | Dir["#{File.dirname(__FILE__)}/examples/**/*.rb"].each { |f| require f }
42 |
--------------------------------------------------------------------------------
/spec/support/requirements.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 | module Spec
3 |
4 | #
5 | # Define the interface methods required to
6 | # use an adapter shared example
7 | #
8 | #
9 | module AdapterRequirements
10 |
11 | extend ActiveSupport::Concern
12 |
13 | included do
14 | let(:db1){ Apartment::Test.next_db }
15 | let(:db2){ Apartment::Test.next_db }
16 | let(:connection){ ActiveRecord::Base.connection }
17 |
18 | before do
19 | ActiveRecord::Base.establish_connection config
20 | subject.create(db1)
21 | subject.create(db2)
22 | end
23 |
24 | after do
25 | # Reset before dropping (can't drop a db you're connected to)
26 | subject.reset
27 |
28 | # sometimes we manually drop these schemas in testing, don't care if we can't drop, hence rescue
29 | subject.drop(db1) rescue true
30 | subject.drop(db2) rescue true
31 |
32 | ActiveRecord::Base.clear_all_connections!
33 | Apartment::Database.reload!
34 | end
35 | end
36 |
37 | %w{subject config database_names default_database}.each do |method|
38 | define_method method do
39 | raise "You must define a `#{method}` method in your host group"
40 | end unless defined?(method)
41 | end
42 |
43 | end
44 | end
45 | end
--------------------------------------------------------------------------------
/spec/adapters/postgresql_adapter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'apartment/adapters/postgresql_adapter'
3 |
4 | describe Apartment::Adapters::PostgresqlAdapter do
5 |
6 | let(:config){ Apartment::Test.config['connections']['postgresql'] }
7 | subject{ Apartment::Database.postgresql_adapter config.symbolize_keys }
8 |
9 | context "using schemas" do
10 |
11 | before{ Apartment.use_postgres_schemas = true }
12 |
13 | # Not sure why, but somehow using let(:database_names) memoizes for the whole example group, not just each test
14 | def database_names
15 | ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect{|row| row['nspname']}
16 | end
17 |
18 | let(:default_database){ subject.process{ ActiveRecord::Base.connection.schema_search_path } }
19 |
20 | it_should_behave_like "a generic apartment adapter"
21 | it_should_behave_like "a schema based apartment adapter"
22 | end
23 |
24 | context "using databases" do
25 |
26 | before{ Apartment.use_postgres_schemas = false }
27 |
28 | # Not sure why, but somehow using let(:database_names) memoizes for the whole example group, not just each test
29 | def database_names
30 | connection.execute("select datname from pg_database;").collect{|row| row['datname']}
31 | end
32 |
33 | let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
34 |
35 | it_should_behave_like "a generic apartment adapter"
36 | it_should_behave_like "a db based apartment adapter"
37 | end
38 | end
--------------------------------------------------------------------------------
/apartment.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $: << File.expand_path("../lib", __FILE__)
3 | require "apartment/version"
4 |
5 | Gem::Specification.new do |s|
6 | s.name = %q{apartment}
7 | s.version = Apartment::VERSION
8 |
9 | s.authors = ["Ryan Brunner", "Brad Robertson"]
10 | s.summary = %q{A Ruby gem for managing database multitenancy in Rails applications}
11 | s.description = %q{Apartment allows Rails applications to deal with database multitenancy}
12 | s.email = %w{ryan@ryanbrunner.com bradleyrobertson@gmail.com}
13 | s.files = `git ls-files`.split("\n")
14 | s.test_files = `git ls-files -- {spec}/*`.split("\n")
15 |
16 | s.homepage = %q{http://github.com/bradrobertson/apartment}
17 | s.licenses = ["MIT"]
18 | s.require_paths = ["lib"]
19 | s.rubygems_version = %q{1.3.7}
20 |
21 | s.add_dependency 'activerecord', '>= 3.1.2' # must be >= 3.1.2 due to bug in prepared_statements
22 | s.add_dependency 'rack', '>= 1.3.6'
23 |
24 | s.add_development_dependency 'pry', '~> 0.9.9'
25 | s.add_development_dependency 'rails', '>= 3.1.2'
26 | s.add_development_dependency 'rake', '~> 0.9.2'
27 | s.add_development_dependency 'sqlite3'
28 | s.add_development_dependency 'rspec', '~> 2.10.0'
29 | s.add_development_dependency 'rspec-rails', '~> 2.10.0'
30 | s.add_development_dependency 'capybara', '~> 1.0.0'
31 | s.add_development_dependency 'pg', '>= 0.11.0'
32 | s.add_development_dependency 'mysql2', '~> 0.3.10'
33 | s.add_development_dependency 'delayed_job', '~> 3.0'
34 | s.add_development_dependency 'delayed_job_active_record'
35 | end
36 |
--------------------------------------------------------------------------------
/lib/apartment/database.rb:
--------------------------------------------------------------------------------
1 | require 'active_support/core_ext/module/delegation'
2 |
3 | module Apartment
4 |
5 | # The main entry point to Apartment functions
6 | #
7 | module Database
8 |
9 | extend self
10 |
11 | delegate :create, :current_database, :current, :drop, :process, :process_excluded_models, :reset, :seed, :switch, :to => :adapter
12 |
13 | attr_writer :config
14 |
15 | # Initialize Apartment config options such as excluded_models
16 | #
17 | def init
18 | process_excluded_models
19 | end
20 |
21 | # Fetch the proper multi-tenant adapter based on Rails config
22 | #
23 | # @return {subclass of Apartment::AbstractAdapter}
24 | #
25 | def adapter
26 | @adapter ||= begin
27 | adapter_method = "#{config[:adapter]}_adapter"
28 |
29 | begin
30 | require "apartment/adapters/#{adapter_method}"
31 | rescue LoadError
32 | raise "The adapter `#{config[:adapter]}` is not yet supported"
33 | end
34 |
35 | unless respond_to?(adapter_method)
36 | raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
37 | end
38 |
39 | send(adapter_method, config)
40 | end
41 | end
42 |
43 | # Reset config and adapter so they are regenerated
44 | #
45 | def reload!
46 | @adapter = nil
47 | @config = nil
48 | end
49 |
50 | private
51 |
52 | # Fetch the rails database configuration
53 | #
54 | def config
55 | @config ||= Rails.configuration.database_configuration[Rails.env].symbolize_keys
56 | end
57 |
58 | end
59 |
60 | end
--------------------------------------------------------------------------------
/spec/dummy/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Log error messages when you accidentally call methods on nil.
11 | config.whiny_nils = true
12 |
13 | # Show full error reports and disable caching
14 | config.consider_all_requests_local = true
15 | config.action_controller.perform_caching = false
16 |
17 | # Raise exceptions instead of rendering exception templates
18 | config.action_dispatch.show_exceptions = false
19 |
20 | # Disable request forgery protection in test environment
21 | config.action_controller.allow_forgery_protection = false
22 |
23 | # Tell Action Mailer not to deliver emails to the real world.
24 | # The :test delivery method accumulates sent emails in the
25 | # ActionMailer::Base.deliveries array.
26 | config.action_mailer.delivery_method = :test
27 |
28 | # Use SQL instead of Active Record's schema dumper when creating the test database.
29 | # This is necessary if your schema can't be completely dumped by the schema dumper,
30 | # like if you have constraints or database-specific column types
31 | # config.active_record.schema_format = :sql
32 |
33 | # Print deprecation notices to the stderr
34 | config.active_support.deprecation = :stderr
35 | end
36 |
--------------------------------------------------------------------------------
/spec/support/contexts.rb:
--------------------------------------------------------------------------------
1 | # Some shared contexts for specs
2 |
3 | shared_context "with default schema", :default_schema => true do
4 | let(:default_schema){ Apartment::Test.next_db }
5 |
6 | before do
7 | Apartment::Test.create_schema(default_schema)
8 | Apartment.default_schema = default_schema
9 | end
10 |
11 | after do
12 | # resetting default_schema so we can drop and any further resets won't try to access droppped schema
13 | Apartment.default_schema = nil
14 | Apartment::Test.drop_schema(default_schema)
15 | end
16 | end
17 |
18 | # Some default setup for elevator specs
19 | shared_context "elevators", :elevator => true do
20 | let(:company1) { mock_model(Company, :database => Apartment::Test.next_db).as_null_object }
21 | let(:company2) { mock_model(Company, :database => Apartment::Test.next_db).as_null_object }
22 |
23 | let(:database1) { company1.database }
24 | let(:database2) { company2.database }
25 |
26 | let(:api) { Apartment::Database }
27 |
28 | before do
29 | Apartment.reset # reset all config
30 | Apartment.seed_after_create = false
31 | Apartment.use_postgres_schemas = true
32 | api.reload! # reload adapter
33 |
34 | api.create(database1)
35 | api.create(database2)
36 | end
37 |
38 | after do
39 | api.drop(database1)
40 | api.drop(database2)
41 | end
42 | end
43 |
44 | shared_context "persistent_schemas", :persistent_schemas => true do
45 | let(:persistent_schemas){ ['hstore', 'postgis'] }
46 |
47 | before do
48 | persistent_schemas.map{|schema| subject.create(schema) }
49 | Apartment.persistent_schemas = persistent_schemas
50 | end
51 |
52 | after do
53 | Apartment.persistent_schemas = []
54 | persistent_schemas.map{|schema| subject.drop(schema) }
55 | end
56 | end
--------------------------------------------------------------------------------
/spec/dummy/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended to check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(:version => 20111202022214) do
15 |
16 | create_table "books", :force => true do |t|
17 | t.string "name"
18 | t.integer "pages"
19 | t.datetime "published"
20 | end
21 |
22 | create_table "companies", :force => true do |t|
23 | t.boolean "dummy"
24 | t.string "database"
25 | end
26 |
27 | create_table "delayed_jobs", :force => true do |t|
28 | t.integer "priority", :default => 0
29 | t.integer "attempts", :default => 0
30 | t.text "handler"
31 | t.text "last_error"
32 | t.datetime "run_at"
33 | t.datetime "locked_at"
34 | t.datetime "failed_at"
35 | t.string "locked_by"
36 | t.datetime "created_at"
37 | t.datetime "updated_at"
38 | t.string "queue"
39 | end
40 |
41 | add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
42 |
43 | create_table "users", :force => true do |t|
44 | t.string "name"
45 | t.datetime "birthdate"
46 | t.string "sex"
47 | end
48 |
49 | end
50 |
--------------------------------------------------------------------------------
/lib/apartment/railtie.rb:
--------------------------------------------------------------------------------
1 | require 'rails'
2 |
3 | module Apartment
4 | class Railtie < Rails::Railtie
5 |
6 | #
7 | # Set up our default config options
8 | # Do this before the app initializers run so we don't override custom settings
9 | #
10 | config.before_initialize do
11 | Apartment.configure do |config|
12 | config.excluded_models = []
13 | config.use_postgres_schemas = true
14 | config.database_names = []
15 | config.seed_after_create = false
16 | config.prepend_environment = false
17 | end
18 | end
19 |
20 | # Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized
21 | # Note that this doens't entirely work as expected in Development, because this is called before classes are reloaded
22 | # See the above middleware/console declarations below to help with this. Hope to fix that soon.
23 | #
24 | config.to_prepare do
25 | Apartment::Database.init
26 | end
27 |
28 | #
29 | # Ensure rake tasks are loaded
30 | #
31 | rake_tasks do
32 | load 'tasks/apartment.rake'
33 | end
34 |
35 | #
36 | # The following initializers are a workaround to the fact that I can't properly hook into the rails reloader
37 | # Note this is technically valid for any environment where cache_classes is false, for us, it's just development
38 | #
39 | if Rails.env.development?
40 |
41 | # Apartment::Reloader is middleware to initialize things properly on each request to dev
42 | initializer 'apartment.init' do |app|
43 | app.config.middleware.use "Apartment::Reloader"
44 | end
45 |
46 | # Overrides reload! to also call Apartment::Database.init as well so that the reloaded classes have the proper table_names
47 | console do
48 | require 'apartment/console'
49 | end
50 |
51 | end
52 |
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/spec/dummy/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Dummy::Application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb
3 |
4 | # The production environment is meant for finished, "live" apps.
5 | # Code is not reloaded between requests
6 | config.cache_classes = true
7 |
8 | # Full error reports are disabled and caching is turned on
9 | config.consider_all_requests_local = false
10 | config.action_controller.perform_caching = true
11 |
12 | # Specifies the header that your server uses for sending files
13 | config.action_dispatch.x_sendfile_header = "X-Sendfile"
14 |
15 | # For nginx:
16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
17 |
18 | # If you have no front-end server that supports something like X-Sendfile,
19 | # just comment this out and Rails will serve the files
20 |
21 | # See everything in the log (default is :info)
22 | # config.log_level = :debug
23 |
24 | # Use a different logger for distributed setups
25 | # config.logger = SyslogLogger.new
26 |
27 | # Use a different cache store in production
28 | # config.cache_store = :mem_cache_store
29 |
30 | # Disable Rails's static asset server
31 | # In production, Apache or nginx will already do this
32 | config.serve_static_assets = false
33 |
34 | # Enable serving of images, stylesheets, and javascripts from an asset server
35 | # config.action_controller.asset_host = "http://assets.example.com"
36 |
37 | # Disable delivery errors, bad email addresses will be ignored
38 | # config.action_mailer.raise_delivery_errors = false
39 |
40 | # Enable threaded mode
41 | # config.threadsafe!
42 |
43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
44 | # the I18n.default_locale when a translation can not be found)
45 | config.i18n.fallbacks = true
46 |
47 | # Send deprecation notices to registered listeners
48 | config.active_support.deprecation = :notify
49 | end
50 |
--------------------------------------------------------------------------------
/spec/unit/config_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment do
4 |
5 | describe "#config" do
6 |
7 | let(:excluded_models){ [Company] }
8 |
9 | it "should yield the Apartment object" do
10 | Apartment.configure do |config|
11 | config.excluded_models = []
12 | config.should == Apartment
13 | end
14 | end
15 |
16 | it "should set excluded models" do
17 | Apartment.configure do |config|
18 | config.excluded_models = excluded_models
19 | end
20 | Apartment.excluded_models.should == excluded_models
21 | end
22 |
23 | it "should set postgres_schemas" do
24 | Apartment.configure do |config|
25 | config.excluded_models = []
26 | config.use_postgres_schemas = false
27 | end
28 | Apartment.use_postgres_schemas.should be_false
29 | end
30 |
31 | it "should set seed_after_create" do
32 | Apartment.configure do |config|
33 | config.excluded_models = []
34 | config.seed_after_create = true
35 | end
36 | Apartment.seed_after_create.should be_true
37 | end
38 |
39 | context "databases" do
40 | it "should return object if it doesnt respond_to call" do
41 | database_names = ['users', 'companies']
42 |
43 | Apartment.configure do |config|
44 | config.excluded_models = []
45 | config.database_names = database_names
46 | end
47 | Apartment.database_names.should == database_names
48 | end
49 |
50 | it "should invoke the proc if appropriate" do
51 | database_names = lambda{ ['users', 'users'] }
52 | database_names.should_receive(:call)
53 |
54 | Apartment.configure do |config|
55 | config.excluded_models = []
56 | config.database_names = database_names
57 | end
58 | Apartment.database_names
59 | end
60 |
61 | it "should return the invoked proc if appropriate" do
62 | dbs = lambda{ Company.scoped }
63 |
64 | Apartment.configure do |config|
65 | config.excluded_models = []
66 | config.database_names = dbs
67 | end
68 |
69 | Apartment.database_names.should == Company.scoped
70 | end
71 | end
72 |
73 | end
74 | end
--------------------------------------------------------------------------------
/lib/tasks/apartment.rake:
--------------------------------------------------------------------------------
1 | apartment_namespace = namespace :apartment do
2 |
3 | desc "Migrate all multi-tenant databases"
4 | task :migrate => 'db:migrate' do
5 |
6 | Apartment.database_names.each do |db|
7 | puts("Migrating #{db} database")
8 | Apartment::Migrator.migrate db
9 | end
10 | end
11 |
12 | desc "Seed all multi-tenant databases"
13 | task :seed => 'db:seed' do
14 |
15 | Apartment.database_names.each do |db|
16 | puts("Seeding #{db} database")
17 | Apartment::Database.process(db) do
18 | Apartment::Database.seed
19 | end
20 | end
21 | end
22 |
23 | desc "Rolls the schema back to the previous version (specify steps w/ STEP=n) across all multi-tenant dbs."
24 | task :rollback => 'db:rollback' do
25 | step = ENV['STEP'] ? ENV['STEP'].to_i : 1
26 |
27 | Apartment.database_names.each do |db|
28 | puts("Rolling back #{db} database")
29 | Apartment::Migrator.rollback db, step
30 | end
31 | end
32 |
33 | namespace :migrate do
34 |
35 | desc 'Runs the "up" for a given migration VERSION across all multi-tenant dbs.'
36 | task :up => 'db:migrate:up' do
37 | version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
38 | raise 'VERSION is required' unless version
39 |
40 | Apartment.database_names.each do |db|
41 | puts("Migrating #{db} database up")
42 | Apartment::Migrator.run :up, db, version
43 | end
44 | end
45 |
46 | desc 'Runs the "down" for a given migration VERSION across all multi-tenant dbs.'
47 | task :down => 'db:migrate:down' do
48 | version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
49 | raise 'VERSION is required' unless version
50 |
51 | Apartment.database_names.each do |db|
52 | puts("Migrating #{db} database down")
53 | Apartment::Migrator.run :down, db, version
54 | end
55 | end
56 |
57 | desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
58 | task :redo => 'db:migrate:redo' do
59 | if ENV['VERSION']
60 | apartment_namespace['migrate:down'].invoke
61 | apartment_namespace['migrate:up'].invoke
62 | else
63 | apartment_namespace['rollback'].invoke
64 | apartment_namespace['migrate'].invoke
65 | end
66 | end
67 |
68 | end
69 |
70 | end
--------------------------------------------------------------------------------
/spec/integration/apartment_rake_integration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rake'
3 |
4 | describe "apartment rake tasks" do
5 |
6 | before do
7 | @rake = Rake::Application.new
8 | Rake.application = @rake
9 | Dummy::Application.load_tasks
10 |
11 | # somehow this misc.rake file gets lost in the shuffle
12 | # it defines a `rails_env` task that our db:migrate depends on
13 | # No idea why, but during the tests, we somehow lose this tasks, so we get an error when testing migrations
14 | # This is STUPID!
15 | load "rails/tasks/misc.rake"
16 | end
17 |
18 | after do
19 | Rake.application = nil
20 | end
21 |
22 | before do
23 | Apartment.configure do |config|
24 | config.excluded_models = ["Company"]
25 | config.database_names = lambda{ Company.scoped.collect(&:database) }
26 | end
27 |
28 | # fix up table name of shared/excluded models
29 | Company.table_name = 'public.companies'
30 | end
31 |
32 | context "with x number of databases" do
33 |
34 | let(:x){ 1 + rand(5) } # random number of dbs to create
35 | let(:db_names){ x.times.map{ Apartment::Test.next_db } }
36 | let!(:company_count){ Company.count + db_names.length }
37 |
38 | before do
39 | db_names.collect do |db_name|
40 | Apartment::Database.create(db_name)
41 | Company.create :database => db_name
42 | end
43 | end
44 |
45 | after do
46 | db_names.each{ |db| Apartment::Database.drop(db) }
47 | Company.delete_all
48 | end
49 |
50 | describe "#migrate" do
51 | it "should migrate all databases" do
52 | Apartment::Migrator.should_receive(:migrate).exactly(company_count).times
53 |
54 | @rake['apartment:migrate'].invoke
55 | end
56 | end
57 |
58 | describe "#rollback" do
59 | it "should rollback all dbs" do
60 | db_names.each do |name|
61 | Apartment::Migrator.should_receive(:rollback).with(name, anything)
62 | end
63 |
64 | @rake['apartment:rollback'].invoke
65 | @rake['apartment:migrate'].invoke # migrate again so that our next test 'seed' can run (requires migrations to be complete)
66 | end
67 | end
68 |
69 | describe "apartment:seed" do
70 | it "should seed all databases" do
71 | Apartment::Database.should_receive(:seed).exactly(company_count).times
72 |
73 | @rake['apartment:seed'].invoke
74 | end
75 | end
76 |
77 | end
78 | end
--------------------------------------------------------------------------------
/spec/dummy/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require "active_model/railtie"
4 | require "active_record/railtie"
5 | require "action_controller/railtie"
6 | require "action_view/railtie"
7 | require "action_mailer/railtie"
8 |
9 | Bundler.require
10 | require "apartment"
11 |
12 | module Dummy
13 | class Application < Rails::Application
14 | # Settings in config/environments/* take precedence over those specified here.
15 | # Application configuration should go into files in config/initializers
16 | # -- all .rb files in that directory are automatically loaded.
17 |
18 | config.middleware.use 'Apartment::Elevators::Subdomain'
19 | config.middleware.use 'Apartment::Elevators::Domain'
20 | # Our test for this middleware is using a query_string couldn't think of a better way to differentiate it from the other middleware
21 | config.middleware.use 'Apartment::Elevators::Generic', Proc.new { |request| request.query_string.split('=').last if request.query_string.present? }
22 |
23 | # Custom directories with classes and modules you want to be autoloadable.
24 | config.autoload_paths += %W(#{config.root}/lib)
25 |
26 | # Only load the plugins named here, in the order given (default is alphabetical).
27 | # :all can be used as a placeholder for all plugins not explicitly named.
28 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
29 |
30 | # Activate observers that should always be running.
31 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
32 |
33 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
34 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
35 | # config.time_zone = 'Central Time (US & Canada)'
36 |
37 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
38 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
39 | # config.i18n.default_locale = :de
40 |
41 | # JavaScript files you want as :defaults (application.js is always included).
42 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
43 |
44 | # Configure the default encoding used in templates for Ruby 1.9.
45 | config.encoding = "utf-8"
46 |
47 | # Configure sensitive parameters which will be filtered from the log file.
48 | config.filter_parameters += [:password]
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/integration/delayed_job_integration_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'delayed_job'
3 | require 'delayed_job_active_record'
4 |
5 | describe Apartment::Delayed do
6 |
7 | # See apartment.yml file in dummy app config
8 |
9 | let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
10 | let(:database){ Apartment::Test.next_db }
11 | let(:database2){ Apartment::Test.next_db }
12 |
13 | before do
14 | ActiveRecord::Base.establish_connection config
15 | Apartment::Test.load_schema # load the Rails schema in the public db schema
16 | Apartment::Database.stub(:config).and_return config # Use postgresql database config for this test
17 |
18 | Apartment.configure do |config|
19 | config.use_postgres_schemas = true
20 | end
21 |
22 | Apartment::Database.create database
23 | Apartment::Database.create database2
24 | end
25 |
26 | after do
27 | Apartment::Test.drop_schema database
28 | Apartment::Test.drop_schema database2
29 | Apartment.reset
30 | end
31 |
32 | describe Apartment::Delayed::Requirements do
33 |
34 | before do
35 | Apartment::Database.switch database
36 | User.send(:include, Apartment::Delayed::Requirements)
37 | User.create
38 | end
39 |
40 | it "should initialize a database attribute on a class" do
41 | user = User.first
42 | user.database.should == database
43 | end
44 |
45 | it "should not overwrite any previous after_initialize declarations" do
46 | User.class_eval do
47 | after_find :set_name
48 |
49 | def set_name
50 | self.name = "Some Name"
51 | end
52 | end
53 |
54 | user = User.first
55 | user.database.should == database
56 | user.name.should == "Some Name"
57 | end
58 |
59 | it "should set the db on a new record before it saves" do
60 | user = User.create
61 | user.database.should == database
62 | end
63 |
64 | context "serialization" do
65 | it "should serialize the proper database attribute" do
66 | user_yaml = User.first.to_yaml
67 | Apartment::Database.switch database2
68 | user = YAML.load user_yaml
69 | user.database.should == database
70 | end
71 | end
72 | end
73 |
74 | describe Apartment::Delayed::Job::Hooks do
75 |
76 | let(:worker){ Delayed::Worker.new }
77 | let(:job){ Delayed::Job.enqueue User.new }
78 |
79 | it "should switch to previous db" do
80 | Apartment::Database.switch database
81 | worker.run(job)
82 |
83 | Apartment::Database.current_database.should == database
84 | end
85 | end
86 |
87 | end
--------------------------------------------------------------------------------
/lib/apartment.rb:
--------------------------------------------------------------------------------
1 | require 'apartment/railtie' if defined?(Rails)
2 |
3 | module Apartment
4 |
5 | class << self
6 | ACCESSOR_METHODS = [:use_postgres_schemas, :seed_after_create, :prepend_environment]
7 | WRITER_METHODS = [:database_names, :excluded_models, :default_schema, :persistent_schemas]
8 |
9 | attr_accessor(*ACCESSOR_METHODS)
10 | attr_writer(*WRITER_METHODS)
11 |
12 | # configure apartment with available options
13 | def configure
14 | yield self if block_given?
15 | end
16 |
17 | # Be careful not to use `return` here so both Proc and lambda can be used without breaking
18 | def database_names
19 | @database_names.respond_to?(:call) ? @database_names.call : @database_names
20 | end
21 |
22 | # Default to empty array
23 | def excluded_models
24 | @excluded_models || []
25 | end
26 |
27 | def default_schema
28 | @default_schema || "public"
29 | end
30 |
31 | def persistent_schemas
32 | @persistent_schemas || []
33 | end
34 |
35 | # Reset all the config for Apartment
36 | def reset
37 | (ACCESSOR_METHODS + WRITER_METHODS).each{|method| instance_variable_set(:"@#{method}", nil) }
38 | end
39 |
40 | end
41 |
42 | autoload :Database, 'apartment/database'
43 | autoload :Migrator, 'apartment/migrator'
44 | autoload :Reloader, 'apartment/reloader'
45 |
46 | module Adapters
47 | autoload :AbstractAdapter, 'apartment/adapters/abstract_adapter'
48 | # Specific adapters will be loaded dynamically based on adapter in config
49 | end
50 |
51 | module Elevators
52 | autoload :Generic, 'apartment/elevators/generic'
53 | autoload :Subdomain, 'apartment/elevators/subdomain'
54 | autoload :Domain, 'apartment/elevators/domain'
55 | end
56 |
57 | module Delayed
58 |
59 | autoload :Requirements, 'apartment/delayed_job/requirements'
60 |
61 | module Job
62 | autoload :Hooks, 'apartment/delayed_job/hooks'
63 | end
64 | end
65 |
66 | # Exceptions
67 | class ApartmentError < StandardError; end
68 |
69 | # Raised when apartment cannot find the adapter specified in config/database.yml
70 | class AdapterNotFound < ApartmentError; end
71 |
72 | # Raised when database cannot find the specified database
73 | class DatabaseNotFound < ApartmentError; end
74 |
75 | # Raised when trying to create a database that already exists
76 | class DatabaseExists < ApartmentError; end
77 |
78 | # Raised when database cannot find the specified schema
79 | class SchemaNotFound < ApartmentError; end
80 |
81 | # Raised when trying to create a schema that already exists
82 | class SchemaExists < ApartmentError; end
83 |
84 | # Raised when an ActiveRecord object does not have the required database field on it
85 | class DJSerializationError < ApartmentError; end
86 |
87 | end
88 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler' rescue 'You must `gem install bundler` and `bundle install` to run rake tasks'
2 | Bundler.setup
3 | Bundler::GemHelper.install_tasks
4 |
5 | require "rspec"
6 | require "rspec/core/rake_task"
7 |
8 | RSpec::Core::RakeTask.new(:spec => %w{ db:copy_credentials db:test:prepare }) do |spec|
9 | spec.pattern = "spec/**/*_spec.rb"
10 | # spec.rspec_opts = '--order rand:16996'
11 | end
12 |
13 | namespace :spec do
14 |
15 | [:tasks, :unit, :adapters, :integration].each do |type|
16 | RSpec::Core::RakeTask.new(type => :spec) do |spec|
17 | spec.pattern = "spec/#{type}/**/*_spec.rb"
18 | end
19 | end
20 |
21 | end
22 |
23 | task :default => :spec
24 |
25 | namespace :db do
26 | namespace :test do
27 | task :prepare => %w{postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db}
28 | end
29 |
30 | desc "copy sample database credential files over if real files don't exist"
31 | task :copy_credentials do
32 | require 'fileutils'
33 | apartment_db_file = 'spec/config/database.yml'
34 | rails_db_file = 'spec/dummy/config/database.yml'
35 |
36 | FileUtils.copy(apartment_db_file + '.sample', apartment_db_file, :verbose => true) unless File.exists?(apartment_db_file)
37 | FileUtils.copy(rails_db_file + '.sample', rails_db_file, :verbose => true) unless File.exists?(rails_db_file)
38 | end
39 | end
40 |
41 | namespace :postgres do
42 | require 'active_record'
43 | require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
44 |
45 | desc 'Build the PostgreSQL test databases'
46 | task :build_db do
47 | %x{ createdb -E UTF8 #{pg_config['database']} -Upostgres } rescue "test db already exists"
48 | ActiveRecord::Base.establish_connection pg_config
49 | ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
50 | end
51 |
52 | desc "drop the PostgreSQL test database"
53 | task :drop_db do
54 | puts "dropping database #{pg_config['database']}"
55 | %x{ dropdb #{pg_config['database']} -Upostgres }
56 | end
57 |
58 | end
59 |
60 | namespace :mysql do
61 | require 'active_record'
62 | require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
63 |
64 | desc 'Build the MySQL test databases'
65 | task :build_db do
66 | %x{ mysqladmin -u root create #{my_config['database']} } rescue "test db already exists"
67 | ActiveRecord::Base.establish_connection my_config
68 | ActiveRecord::Migrator.migrate('spec/dummy/db/migrate')
69 | end
70 |
71 | desc "drop the MySQL test database"
72 | task :drop_db do
73 | puts "dropping database #{my_config['database']}"
74 | %x{ mysqladmin -u root drop #{my_config['database']} --force}
75 | end
76 |
77 | end
78 |
79 | # TODO clean this up
80 | def config
81 | Apartment::Test.config['connections']
82 | end
83 |
84 | def pg_config
85 | config['postgresql']
86 | end
87 |
88 | def my_config
89 | config['mysql']
90 | end
91 |
--------------------------------------------------------------------------------
/spec/examples/generic_adapter_examples.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | shared_examples_for "a generic apartment adapter" do
4 | include Apartment::Spec::AdapterRequirements
5 |
6 | before{ Apartment.prepend_environment = false }
7 |
8 | #
9 | # Creates happen already in our before_filter
10 | #
11 | describe "#create" do
12 |
13 | it "should create the new databases" do
14 | database_names.should include(db1)
15 | database_names.should include(db2)
16 | end
17 |
18 | it "should load schema.rb to new schema" do
19 | subject.process(db1) do
20 | connection.tables.should include('companies')
21 | end
22 | end
23 |
24 | it "should yield to block if passed and reset" do
25 | subject.drop(db2) # so we don't get errors on creation
26 |
27 | @count = 0 # set our variable so its visible in and outside of blocks
28 |
29 | subject.create(db2) do
30 | @count = User.count
31 | subject.current_database.should == db2
32 | User.create
33 | end
34 |
35 | subject.current_database.should_not == db2
36 |
37 | subject.process(db2){ User.count.should == @count + 1 }
38 | end
39 | end
40 |
41 | describe "#drop" do
42 | it "should remove the db" do
43 | subject.drop db1
44 | database_names.should_not include(db1)
45 | end
46 | end
47 |
48 | describe "#process" do
49 | it "should connect" do
50 | subject.process(db1) do
51 | subject.current_database.should == db1
52 | end
53 | end
54 |
55 | it "should reset" do
56 | subject.process(db1)
57 | subject.current_database.should == default_database
58 | end
59 |
60 | # We're often finding when using Apartment in tests, the `current_database` (ie the previously connect to db)
61 | # gets dropped, but process will try to return to that db in a test. We should just reset if it doesn't exist
62 | it "should not throw exception if current_database is no longer accessible" do
63 | subject.switch(db2)
64 |
65 | expect {
66 | subject.process(db1){ subject.drop(db2) }
67 | }.to_not raise_error
68 | end
69 | end
70 |
71 | describe "#reset" do
72 | it "should reset connection" do
73 | subject.switch(db1)
74 | subject.reset
75 | subject.current_database.should == default_database
76 | end
77 | end
78 |
79 | describe "#switch" do
80 | it "should connect to new db" do
81 | subject.switch(db1)
82 | subject.current_database.should == db1
83 | end
84 |
85 | it "should reset connection if database is nil" do
86 | subject.switch
87 | subject.current_database.should == default_database
88 | end
89 | end
90 |
91 | describe "#current_database" do
92 | it "should return the current db name" do
93 | subject.switch(db1)
94 | subject.current_database.should == db1
95 | end
96 | end
97 | end
--------------------------------------------------------------------------------
/spec/unit/migrator_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Migrator do
4 |
5 | let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
6 | let(:schema_name){ Apartment::Test.next_db }
7 | let(:version){ 20110613152810 } # note this is brittle! I've literally just taken the version of the one migration I made... don't change this version
8 |
9 | before do
10 | ActiveRecord::Base.establish_connection config
11 | Apartment::Database.stub(:config).and_return config # Use postgresql config for this test
12 | @original_schema = ActiveRecord::Base.connection.schema_search_path
13 |
14 | Apartment.configure do |config|
15 | config.use_postgres_schemas = true
16 | config.excluded_models = []
17 | config.database_names = [schema_name]
18 | end
19 |
20 | Apartment::Database.create schema_name # create the schema
21 | migrations_path = Rails.root + ActiveRecord::Migrator.migrations_path # tell AR where the real migrations are
22 | ActiveRecord::Migrator.stub(:migrations_path).and_return(migrations_path)
23 | end
24 |
25 | after do
26 | Apartment::Test.drop_schema(schema_name)
27 | end
28 |
29 | context "postgresql" do
30 |
31 | context "using schemas" do
32 |
33 | describe "#migrate" do
34 | it "should connect to new db, then reset when done" do
35 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(schema_name).once
36 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(@original_schema).once
37 | Apartment::Migrator.migrate(schema_name)
38 | end
39 |
40 | it "should migrate db" do
41 | ActiveRecord::Migrator.should_receive(:migrate)
42 | Apartment::Migrator.migrate(schema_name)
43 | end
44 | end
45 |
46 | describe "#run" do
47 | context "up" do
48 |
49 | it "should connect to new db, then reset when done" do
50 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(schema_name).once
51 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(@original_schema).once
52 | Apartment::Migrator.run(:up, schema_name, version)
53 | end
54 |
55 | it "should migrate to a version" do
56 | ActiveRecord::Migrator.should_receive(:run).with(:up, anything, version)
57 | Apartment::Migrator.run(:up, schema_name, version)
58 | end
59 | end
60 |
61 | describe "down" do
62 |
63 | it "should connect to new db, then reset when done" do
64 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(schema_name).once
65 | ActiveRecord::Base.connection.should_receive(:schema_search_path=).with(@original_schema).once
66 | Apartment::Migrator.run(:down, schema_name, version)
67 | end
68 |
69 | it "should migrate to a version" do
70 | ActiveRecord::Migrator.should_receive(:run).with(:down, anything, version)
71 | Apartment::Migrator.run(:down, schema_name, version)
72 | end
73 | end
74 | end
75 |
76 | describe "#rollback" do
77 | let(:steps){ 3 }
78 |
79 | it "should rollback the db" do
80 | ActiveRecord::Migrator.should_receive(:rollback).with(anything, steps)
81 | Apartment::Migrator.rollback(schema_name, steps)
82 | end
83 | end
84 | end
85 | end
86 |
87 | end
--------------------------------------------------------------------------------
/spec/tasks/apartment_rake_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'rake'
3 |
4 | describe "apartment rake tasks" do
5 |
6 | before do
7 | @rake = Rake::Application.new
8 | Rake.application = @rake
9 | load 'tasks/apartment.rake'
10 | # stub out rails tasks
11 | Rake::Task.define_task('db:migrate')
12 | Rake::Task.define_task('db:seed')
13 | Rake::Task.define_task('db:rollback')
14 | Rake::Task.define_task('db:migrate:up')
15 | Rake::Task.define_task('db:migrate:down')
16 | Rake::Task.define_task('db:migrate:redo')
17 | end
18 |
19 | after do
20 | Rake.application = nil
21 | ENV['VERSION'] = nil # linux users reported env variable carrying on between tests
22 | end
23 |
24 | let(:version){ '1234' }
25 |
26 | context 'database migration' do
27 |
28 | let(:database_names){ 3.times.map{ Apartment::Test.next_db } }
29 | let(:db_count){ database_names.length }
30 |
31 | before do
32 | Apartment.stub(:database_names).and_return database_names
33 | end
34 |
35 | describe "apartment:migrate" do
36 | before do
37 | ActiveRecord::Migrator.stub(:migrate) # don't care about this
38 | end
39 |
40 | it "should migrate public and all multi-tenant dbs" do
41 | Apartment::Migrator.should_receive(:migrate).exactly(db_count).times
42 | @rake['apartment:migrate'].invoke
43 | end
44 | end
45 |
46 | describe "apartment:migrate:up" do
47 |
48 | context "without a version" do
49 | before do
50 | ENV['VERSION'] = nil
51 | end
52 |
53 | it "requires a version to migrate to" do
54 | lambda{
55 | @rake['apartment:migrate:up'].invoke
56 | }.should raise_error("VERSION is required")
57 | end
58 | end
59 |
60 | context "with version" do
61 |
62 | before do
63 | ENV['VERSION'] = version
64 | end
65 |
66 | it "migrates up to a specific version" do
67 | Apartment::Migrator.should_receive(:run).with(:up, anything, version.to_i).exactly(db_count).times
68 | @rake['apartment:migrate:up'].invoke
69 | end
70 | end
71 | end
72 |
73 | describe "apartment:migrate:down" do
74 |
75 | context "without a version" do
76 | before do
77 | ENV['VERSION'] = nil
78 | end
79 |
80 | it "requires a version to migrate to" do
81 | lambda{
82 | @rake['apartment:migrate:down'].invoke
83 | }.should raise_error("VERSION is required")
84 | end
85 | end
86 |
87 | context "with version" do
88 |
89 | before do
90 | ENV['VERSION'] = version
91 | end
92 |
93 | it "migrates up to a specific version" do
94 | Apartment::Migrator.should_receive(:run).with(:down, anything, version.to_i).exactly(db_count).times
95 | @rake['apartment:migrate:down'].invoke
96 | end
97 | end
98 | end
99 |
100 | describe "apartment:rollback" do
101 |
102 | let(:step){ '3' }
103 |
104 | it "should rollback dbs" do
105 | Apartment::Migrator.should_receive(:rollback).exactly(db_count).times
106 | @rake['apartment:rollback'].invoke
107 | end
108 |
109 | it "should rollback dbs STEP amt" do
110 | Apartment::Migrator.should_receive(:rollback).with(anything, step.to_i).exactly(db_count).times
111 | ENV['STEP'] = step
112 | @rake['apartment:rollback'].invoke
113 | end
114 | end
115 |
116 | end
117 |
118 | end
--------------------------------------------------------------------------------
/spec/database_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Apartment::Database do
4 | context "using mysql" do
5 | # See apartment.yml file in dummy app config
6 |
7 | let(:config){ Apartment::Test.config['connections']['mysql'].symbolize_keys }
8 |
9 | before do
10 | ActiveRecord::Base.establish_connection config
11 | Apartment::Test.load_schema # load the Rails schema in the public db schema
12 | subject.stub(:config).and_return config # Use postgresql database config for this test
13 | end
14 |
15 | describe "#adapter" do
16 | before do
17 | subject.reload!
18 | end
19 |
20 | it "should load mysql adapter" do
21 | subject.adapter
22 | Apartment::Adapters::Mysql2Adapter.should be_a(Class)
23 | end
24 |
25 | end
26 | end
27 |
28 | context "using postgresql" do
29 |
30 | # See apartment.yml file in dummy app config
31 |
32 | let(:config){ Apartment::Test.config['connections']['postgresql'].symbolize_keys }
33 | let(:database){ Apartment::Test.next_db }
34 | let(:database2){ Apartment::Test.next_db }
35 |
36 | before do
37 | Apartment.use_postgres_schemas = true
38 | ActiveRecord::Base.establish_connection config
39 | Apartment::Test.load_schema # load the Rails schema in the public db schema
40 | subject.stub(:config).and_return config # Use postgresql database config for this test
41 | end
42 |
43 | describe "#adapter" do
44 | before do
45 | subject.reload!
46 | end
47 |
48 | it "should load postgresql adapter" do
49 | subject.adapter
50 | Apartment::Adapters::PostgresqlAdapter.should be_a(Class)
51 | end
52 |
53 | it "should raise exception with invalid adapter specified" do
54 | subject.stub(:config).and_return config.merge(:adapter => 'unkown')
55 |
56 | expect {
57 | Apartment::Database.adapter
58 | }.to raise_error
59 | end
60 |
61 | end
62 |
63 | context "with schemas" do
64 |
65 | before do
66 | Apartment.configure do |config|
67 | config.excluded_models = []
68 | config.use_postgres_schemas = true
69 | config.seed_after_create = true
70 | end
71 | subject.create database
72 | end
73 |
74 | after{ subject.drop database }
75 |
76 | describe "#create" do
77 | it "should seed data" do
78 | subject.switch database
79 | User.count.should be > 0
80 | end
81 | end
82 |
83 | describe "#switch" do
84 |
85 | let(:x){ rand(3) }
86 |
87 | context "creating models" do
88 |
89 | before{ subject.create database2 }
90 | after{ subject.drop database2 }
91 |
92 | it "should create a model instance in the current schema" do
93 | subject.switch database2
94 | db2_count = User.count + x.times{ User.create }
95 |
96 | subject.switch database
97 | db_count = User.count + x.times{ User.create }
98 |
99 | subject.switch database2
100 | User.count.should == db2_count
101 |
102 | subject.switch database
103 | User.count.should == db_count
104 | end
105 | end
106 |
107 | context "with excluded models" do
108 |
109 | before do
110 | Apartment.configure do |config|
111 | config.excluded_models = ["Company"]
112 | end
113 | subject.init
114 | end
115 |
116 | it "should create excluded models in public schema" do
117 | subject.reset # ensure we're on public schema
118 | count = Company.count + x.times{ Company.create }
119 |
120 | subject.switch database
121 | x.times{ Company.create }
122 | Company.count.should == count + x
123 | subject.reset
124 | Company.count.should == count + x
125 | end
126 | end
127 |
128 | end
129 |
130 | end
131 |
132 | end
133 | end
134 |
--------------------------------------------------------------------------------
/lib/apartment/adapters/postgresql_adapter.rb:
--------------------------------------------------------------------------------
1 | module Apartment
2 |
3 | module Database
4 |
5 | def self.postgresql_adapter(config)
6 | Apartment.use_postgres_schemas ?
7 | Adapters::PostgresqlSchemaAdapter.new(config) :
8 | Adapters::PostgresqlAdapter.new(config)
9 | end
10 | end
11 |
12 | module Adapters
13 |
14 | # Default adapter when not using Postgresql Schemas
15 | class PostgresqlAdapter < AbstractAdapter
16 |
17 | protected
18 |
19 | # Connect to new database
20 | # Abstract adapter will catch generic ActiveRecord error
21 | # Catch specific adapter errors here
22 | #
23 | # @param {String} database Database name
24 | #
25 | def connect_to_new(database)
26 | super
27 | rescue PGError
28 | raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
29 | end
30 |
31 | end
32 |
33 | # Separate Adapter for Postgresql when using schemas
34 | class PostgresqlSchemaAdapter < AbstractAdapter
35 |
36 | attr_reader :current_database
37 |
38 | # Drop the database schema
39 | #
40 | # @param {String} database Database (schema) to drop
41 | #
42 | def drop(database)
43 | ActiveRecord::Base.connection.execute(%{DROP SCHEMA "#{database}" CASCADE})
44 |
45 | rescue ActiveRecord::StatementInvalid
46 | raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
47 | end
48 |
49 | # Reset search path to default search_path
50 | # Set the table_name to always use the default namespace for excluded models
51 | #
52 | def process_excluded_models
53 | Apartment.excluded_models.each do |excluded_model|
54 | # Note that due to rails reloading, we now take string references to classes rather than
55 | # actual object references. This way when we contantize, we always get the proper class reference
56 | if excluded_model.is_a? Class
57 | warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
58 | excluded_model = excluded_model.name
59 | end
60 |
61 | excluded_model.constantize.tap do |klass|
62 | # some models (such as delayed_job) seem to load and cache their column names before this,
63 | # so would never get the default prefix, so reset first
64 | klass.reset_column_information
65 |
66 | # Ensure that if a schema *was* set, we override
67 | table_name = klass.table_name.split('.', 2).last
68 |
69 | # Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
70 | klass.table_name = "#{Apartment.default_schema}.#{table_name}"
71 | end
72 | end
73 | end
74 |
75 | # Reset schema search path to the default schema_search_path
76 | #
77 | # @return {String} default schema search path
78 | #
79 | def reset
80 | @current_database = Apartment.default_schema
81 | ActiveRecord::Base.connection.schema_search_path = full_search_path
82 | end
83 |
84 | protected
85 |
86 | # Set schema search path to new schema
87 | #
88 | def connect_to_new(database = nil)
89 | return reset if database.nil?
90 |
91 | @current_database = database.to_s
92 | ActiveRecord::Base.connection.schema_search_path = full_search_path
93 |
94 | rescue ActiveRecord::StatementInvalid
95 | raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
96 | end
97 |
98 | # Create the new schema
99 | #
100 | def create_database(database)
101 | ActiveRecord::Base.connection.execute(%{CREATE SCHEMA "#{database}"})
102 |
103 | rescue ActiveRecord::StatementInvalid
104 | raise SchemaExists, "The schema #{database} already exists."
105 | end
106 |
107 | private
108 |
109 | # Generate the final search path to set including persistent_schemas
110 | #
111 | def full_search_path
112 | persistent_schemas = Apartment.persistent_schemas.join(', ')
113 | @current_database.to_s + (persistent_schemas.empty? ? "" : ", #{persistent_schemas}")
114 | end
115 | end
116 | end
117 | end
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 | # 0.16.0
2 | * June 1, 2012
3 |
4 | - Apartment now supports a default_schema to be set, rather than relying on ActiveRecord's default schema_search_path
5 | - Additional schemas can always be maintained in the schema_search_path by configuring persistent_schemas [ryanbrunner]
6 | - This means Hstore is officially supported!!
7 | - There is now a full domain based elevator to switch dbs based on the whole domain [lcowell]
8 | - There is now a generic elevator that takes a Proc to switch dbs based on the return value of that proc.
9 |
10 | # 0.15.0
11 | * March 18, 2012
12 |
13 | - Remove Rails dependency, Apartment can now be used with any Rack based framework using ActiveRecord
14 |
15 | # 0.14.4
16 | * March 8, 2012
17 |
18 | - Delayed::Job Hooks now return to the previous database, rather than resetting
19 |
20 | # 0.14.3
21 | * Feb 21, 2012
22 |
23 | - Fix yaml serialization of non DJ models
24 |
25 | # 0.14.2
26 | * Feb 21, 2012
27 |
28 | - Fix Delayed::Job yaml encoding with Rails > 3.0.x
29 |
30 | # 0.14.1
31 | * Dec 13, 2011
32 |
33 | - Fix ActionDispatch::Callbacks deprecation warnings
34 |
35 | # 0.14.0
36 | * Dec 13, 2011
37 |
38 | - Rails 3.1 Support
39 |
40 | # 0.13.1
41 | * Nov 8, 2011
42 |
43 | - Reset prepared statement cache for rails 3.1.1 before switching dbs when using postgresql schemas
44 | - Only necessary until the next release which will be more schema aware
45 |
46 | # 0.13.0
47 | * Oct 25, 2011
48 |
49 | - `process` will now rescue with reset if the previous schema/db is no longer available
50 | - `create` now takes an optional block which allows you to process within the newly created db
51 | - Fixed Rails version >= 3.0.10 and < 3.1 because there have been significant testing problems with 3.1, next version will hopefully fix this
52 |
53 | # 0.12.0
54 | * Oct 4, 2011
55 |
56 | - Added a `drop` method for removing databases/schemas
57 | - Refactored abstract adapter to further remove duplication in concrete implementations
58 | - Excluded models now take string references so they are properly reloaded in development
59 | - Better silencing of `schema.rb` loading using `verbose` flag
60 |
61 | # 0.11.1
62 | * Sep 22, 2011
63 |
64 | - Better use of Railties for initializing apartment
65 | - The following changes were necessary as I haven't figured out how to properly hook into Rails reloading
66 | - Added reloader middleware in development to init Apartment on each request
67 | - Override `reload!` in console to also init Apartment
68 |
69 | # 0.11.0
70 | * Sep 20, 2011
71 |
72 | - Excluded models no longer use a different connection when using postgresql schemas. Instead their table_name is prefixed with `public.`
73 |
74 | # 0.10.3
75 | * Sep 20, 2011
76 |
77 | - Fix improper raising of exceptions on create and reset
78 |
79 | # 0.10.2
80 | * Sep 15, 2011
81 |
82 | - Remove all the annoying logging for loading db schema and seeding on create
83 |
84 | # 0.10.1
85 | * Aug 11, 2011
86 |
87 | - Fixed bug in DJ where new objects (that hadn't been pulled from the db) didn't have the proper database assigned
88 |
89 | # 0.10.0
90 | * July 29, 2011
91 |
92 | - Added better support for Delayed Job
93 | - New config option that enables Delayed Job wrappers
94 | - Note that DJ support uses a work-around in order to get queues stored in the public schema, not sure why it doesn't work out of the box, will look into it, until then, see documentation on queue'ng jobs
95 |
96 | # 0.9.2
97 | * July 4, 2011
98 |
99 | - Migrations now run associated rails migration fully, fixes schema.rb not being reloaded after migrations
100 |
101 | # 0.9.1
102 | * June 24, 2011
103 |
104 | - Hooks now take the payload object as an argument to fetch the proper db for DJ hooks
105 |
106 | # 0.9.0
107 | * June 23, 2011
108 |
109 | - Added module to provide delayed job hooks
110 |
111 | # 0.8.0
112 | * June 23, 2011
113 |
114 | - Added #current_database which will return the current database (or schema) name
115 |
116 | # 0.7.0
117 | * June 22, 2011
118 |
119 | - Added apartment:seed rake task for seeding all dbs
120 |
121 | # 0.6.0
122 | * June 21, 2011
123 |
124 | - Added #process to connect to new db, perform operations, then ensure a reset
125 |
126 | # 0.5.1
127 | * June 21, 2011
128 |
129 | - Fixed db migrate up/down/rollback
130 | - added db:redo
131 |
132 | # 0.5.0
133 | * June 20, 2011
134 |
135 | - Added the concept of an "Elevator", a rack based strategy for db switching
136 | - Added the Subdomain Elevator middleware to enabled db switching based on subdomain
137 |
138 | # 0.4.0
139 | * June 14, 2011
140 |
141 | - Added `configure` method on Apartment instead of using yml file, allows for dynamic setting of db names to migrate for rake task
142 | - Added `seed_after_create` config option to import seed data to new db on create
143 |
144 | # 0.3.0
145 | * June 10, 2011
146 |
147 | - Added full support for database migration
148 | - Added in method to establish new connection for excluded models on startup rather than on each switch
149 |
150 | # 0.2.0
151 | * June 6, 2011 *
152 |
153 | - Refactor to use more rails/active_support functionality
154 | - Refactor config to lazily load apartment.yml if exists
155 | - Remove OStruct and just use hashes for fetching methods
156 | - Added schema load on create instead of migrating from scratch
157 |
158 | # 0.1.3
159 | * March 30, 2011 *
160 |
161 | - Original pass from Ryan
162 |
163 |
--------------------------------------------------------------------------------
/lib/apartment/adapters/abstract_adapter.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module Apartment
4 |
5 | module Adapters
6 |
7 | class AbstractAdapter
8 |
9 | # @constructor
10 | # @param {Hash} config Database config
11 | # @param {Hash} defaults Some default options
12 | #
13 | def initialize(config, defaults = {})
14 | @config = config
15 | @defaults = defaults
16 | end
17 |
18 | # Create a new database, import schema, seed if appropriate
19 | #
20 | # @param {String} database Database name
21 | #
22 | def create(database)
23 | create_database(database)
24 |
25 | process(database) do
26 | import_database_schema
27 |
28 | # Seed data if appropriate
29 | seed_data if Apartment.seed_after_create
30 |
31 | yield if block_given?
32 | end
33 | end
34 |
35 | # Get the current database name
36 | #
37 | # @return {String} current database name
38 | #
39 | def current_database
40 | ActiveRecord::Base.connection.current_database
41 | end
42 | alias_method :current, :current_database
43 |
44 | # Drop the database
45 | #
46 | # @param {String} database Database name
47 | #
48 | def drop(database)
49 | # ActiveRecord::Base.connection.drop_database note that drop_database will not throw an exception, so manually execute
50 | ActiveRecord::Base.connection.execute("DROP DATABASE #{environmentify(database)}" )
51 |
52 | rescue ActiveRecord::StatementInvalid
53 | raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found"
54 | end
55 |
56 | # Connect to db, do your biz, switch back to previous db
57 | #
58 | # @param {String?} database Database or schema to connect to
59 | #
60 | def process(database = nil)
61 | current_db = current_database
62 | switch(database)
63 | yield if block_given?
64 |
65 | ensure
66 | switch(current_db) rescue reset
67 | end
68 |
69 | # Establish a new connection for each specific excluded model
70 | #
71 | def process_excluded_models
72 | # All other models will shared a connection (at ActiveRecord::Base) and we can modify at will
73 | Apartment.excluded_models.each do |excluded_model|
74 | # Note that due to rails reloading, we now take string references to classes rather than
75 | # actual object references. This way when we contantize, we always get the proper class reference
76 | if excluded_model.is_a? Class
77 | warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
78 | excluded_model = excluded_model.name
79 | end
80 |
81 | excluded_model.constantize.establish_connection @config
82 | end
83 | end
84 |
85 | # Reset the database connection to the default
86 | #
87 | def reset
88 | ActiveRecord::Base.establish_connection @config
89 | end
90 |
91 | # Switch to new connection (or schema if appopriate)
92 | #
93 | # @param {String} database Database name
94 | #
95 | def switch(database = nil)
96 | # Just connect to default db and return
97 | return reset if database.nil?
98 |
99 | connect_to_new(database)
100 | end
101 |
102 | # Load the rails seed file into the db
103 | #
104 | def seed_data
105 | silence_stream(STDOUT){ load_or_abort("#{Rails.root}/db/seeds.rb") } # Don't log the output of seeding the db
106 | end
107 | alias_method :seed, :seed_data
108 |
109 | protected
110 |
111 | # Create the database
112 | #
113 | # @param {String} database Database name
114 | #
115 | def create_database(database)
116 | ActiveRecord::Base.connection.create_database( environmentify(database) )
117 |
118 | rescue ActiveRecord::StatementInvalid
119 | raise DatabaseExists, "The database #{environmentify(database)} already exists."
120 | end
121 |
122 | # Connect to new database
123 | #
124 | # @param {String} database Database name
125 | #
126 | def connect_to_new(database)
127 | ActiveRecord::Base.establish_connection multi_tenantify(database)
128 | ActiveRecord::Base.connection.active? # call active? to manually check if this connection is valid
129 |
130 | rescue ActiveRecord::StatementInvalid
131 | raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found."
132 | end
133 |
134 | # Prepend the environment if configured and the environment isn't already there
135 | #
136 | # @param {String} database Database name
137 | # @return {String} database name with Rails environment *optionally* prepended
138 | #
139 | def environmentify(database)
140 | Apartment.prepend_environment && !database.include?(Rails.env) ? "#{Rails.env}_#{database}" : database
141 | end
142 |
143 | # Import the database schema
144 | #
145 | def import_database_schema
146 | ActiveRecord::Schema.verbose = false # do not log schema load output.
147 | load_or_abort("#{Rails.root}/db/schema.rb")
148 | end
149 |
150 | # Return a new config that is multi-tenanted
151 | #
152 | def multi_tenantify(database)
153 | @config.clone.tap do |config|
154 | config[:database] = environmentify(database)
155 | end
156 | end
157 |
158 | # Load a file or abort if it doesn't exists
159 | #
160 | def load_or_abort(file)
161 | if File.exists?(file)
162 | load(file)
163 | else
164 | abort %{#{file} doesn't exist yet}
165 | end
166 | end
167 |
168 | end
169 | end
170 | end
171 |
--------------------------------------------------------------------------------
/spec/examples/schema_adapter_examples.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | shared_examples_for "a schema based apartment adapter" do
4 | include Apartment::Spec::AdapterRequirements
5 |
6 | let(:schema1){ db1 }
7 | let(:schema2){ db2 }
8 | let(:public_schema){ default_database }
9 |
10 | describe "#init" do
11 |
12 | before do
13 | Apartment.configure do |config|
14 | config.excluded_models = ["Company"]
15 | end
16 | end
17 |
18 | it "should process model exclusions" do
19 | Apartment::Database.init
20 |
21 | Company.table_name.should == "public.companies"
22 | end
23 |
24 | context "with a default_schema", :default_schema => true do
25 |
26 | it "should set the proper table_name on excluded_models" do
27 | Apartment::Database.init
28 |
29 | Company.table_name.should == "#{default_schema}.companies"
30 | end
31 | end
32 | end
33 |
34 | #
35 | # Creates happen already in our before_filter
36 | #
37 | describe "#create" do
38 |
39 | it "should load schema.rb to new schema" do
40 | connection.schema_search_path = schema1
41 | connection.tables.should include('companies')
42 | end
43 |
44 | it "should yield to block if passed and reset" do
45 | subject.drop(schema2) # so we don't get errors on creation
46 |
47 | @count = 0 # set our variable so its visible in and outside of blocks
48 |
49 | subject.create(schema2) do
50 | @count = User.count
51 | connection.schema_search_path.should start_with schema2
52 | User.create
53 | end
54 |
55 | connection.schema_search_path.should_not start_with schema2
56 |
57 | subject.process(schema2){ User.count.should == @count + 1 }
58 | end
59 |
60 | context "numeric database names" do
61 | let(:db){ 1234 }
62 | it "should allow them" do
63 | expect {
64 | subject.create(db)
65 | }.to_not raise_error
66 | database_names.should include(db.to_s)
67 | end
68 |
69 | after{ subject.drop(db) }
70 | end
71 |
72 | end
73 |
74 | describe "#drop" do
75 | it "should raise an error for unknown database" do
76 | expect {
77 | subject.drop "unknown_database"
78 | }.to raise_error(Apartment::SchemaNotFound)
79 | end
80 |
81 | context "numeric database names" do
82 | let(:db){ 1234 }
83 |
84 | it "should be able to drop them" do
85 | subject.create(db)
86 | expect {
87 | subject.drop(db)
88 | }.to_not raise_error
89 | database_names.should_not include(db.to_s)
90 | end
91 |
92 | after { subject.drop(db) rescue nil }
93 | end
94 | end
95 |
96 | describe "#process" do
97 | it "should connect" do
98 | subject.process(schema1) do
99 | connection.schema_search_path.should start_with schema1
100 | end
101 | end
102 |
103 | it "should reset" do
104 | subject.process(schema1)
105 | connection.schema_search_path.should start_with public_schema
106 | end
107 | end
108 |
109 | describe "#reset" do
110 | it "should reset connection" do
111 | subject.switch(schema1)
112 | subject.reset
113 | connection.schema_search_path.should start_with public_schema
114 | end
115 |
116 | context "with default_schema", :default_schema => true do
117 | it "should reset to the default schema" do
118 | subject.switch(schema1)
119 | subject.reset
120 | connection.schema_search_path.should start_with default_schema
121 | end
122 | end
123 |
124 | context "persistent_schemas", :persistent_schemas => true do
125 | before do
126 | subject.switch(schema1)
127 | subject.reset
128 | end
129 |
130 | it "maintains the persistent schemas in the schema_search_path" do
131 | connection.schema_search_path.should end_with persistent_schemas.join(', ')
132 | end
133 |
134 | context "with default_schema", :default_schema => true do
135 | it "prioritizes the switched schema to front of schema_search_path" do
136 | subject.reset # need to re-call this as the default_schema wasn't set at the time that the above reset ran
137 | connection.schema_search_path.should start_with default_schema
138 | end
139 | end
140 | end
141 | end
142 |
143 | describe "#switch" do
144 | it "should connect to new schema" do
145 | subject.switch(schema1)
146 | connection.schema_search_path.should start_with schema1
147 | end
148 |
149 | it "should reset connection if database is nil" do
150 | subject.switch
151 | connection.schema_search_path.should == public_schema
152 | end
153 |
154 | it "should raise an error if schema is invalid" do
155 | expect {
156 | subject.switch 'unknown_schema'
157 | }.to raise_error(Apartment::SchemaNotFound)
158 | end
159 |
160 | context "numeric databases" do
161 | let(:db){ 1234 }
162 |
163 | it "should connect to them" do
164 | subject.create(db)
165 | expect {
166 | subject.switch(db)
167 | }.to_not raise_error
168 |
169 | connection.schema_search_path.should start_with db.to_s
170 | end
171 |
172 | after{ subject.drop(db) }
173 | end
174 |
175 | describe "with default_schema specified", :default_schema => true do
176 | before do
177 | subject.switch(schema1)
178 | end
179 |
180 | it "should switch out the default schema rather than public" do
181 | connection.schema_search_path.should_not include default_schema
182 | end
183 |
184 | it "should still switch to the switched schema" do
185 | connection.schema_search_path.should start_with schema1
186 | end
187 | end
188 |
189 | context "persistent_schemas", :persistent_schemas => true do
190 |
191 | before{ subject.switch(schema1) }
192 |
193 | it "maintains the persistent schemas in the schema_search_path" do
194 | connection.schema_search_path.should end_with persistent_schemas.join(', ')
195 | end
196 |
197 | it "prioritizes the switched schema to front of schema_search_path" do
198 | connection.schema_search_path.should start_with schema1
199 | end
200 | end
201 | end
202 |
203 | describe "#current_database" do
204 | it "should return the current schema name" do
205 | subject.switch(schema1)
206 | subject.current_database.should == schema1
207 | end
208 |
209 | context "persistent_schemas", :persistent_schemas => true do
210 | it "should exlude persistent_schemas" do
211 | subject.switch(schema1)
212 | subject.current_database.should == schema1
213 | end
214 | end
215 | end
216 |
217 | end
--------------------------------------------------------------------------------