├── 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 --------------------------------------------------------------------------------