├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .pryrc ├── .rspec ├── .travis.yml ├── Appraisals ├── Gemfile ├── Guardfile ├── HISTORY.md ├── README.md ├── Rakefile ├── TODO.md ├── apartment.gemspec ├── docker-compose.yml ├── gemfiles ├── rails_4_2.gemfile ├── rails_5_0.gemfile ├── rails_5_1.gemfile ├── rails_5_2.gemfile ├── rails_6_0.gemfile └── rails_master.gemfile ├── lib ├── apartment.rb ├── apartment │ ├── adapters │ │ ├── abstract_adapter.rb │ │ ├── abstract_jdbc_adapter.rb │ │ ├── jdbc_mysql_adapter.rb │ │ ├── jdbc_postgresql_adapter.rb │ │ ├── mysql2_adapter.rb │ │ ├── postgis_adapter.rb │ │ ├── postgresql_adapter.rb │ │ └── sqlite3_adapter.rb │ ├── console.rb │ ├── deprecation.rb │ ├── elevators │ │ ├── domain.rb │ │ ├── first_subdomain.rb │ │ ├── generic.rb │ │ ├── host.rb │ │ ├── host_hash.rb │ │ └── subdomain.rb │ ├── migrator.rb │ ├── railtie.rb │ ├── reloader.rb │ ├── tasks │ │ └── enhancements.rb │ ├── tenant.rb │ └── version.rb ├── generators │ └── apartment │ │ └── install │ │ ├── USAGE │ │ ├── install_generator.rb │ │ └── templates │ │ └── apartment.rb └── tasks │ └── apartment.rake └── spec ├── adapters ├── jdbc_mysql_adapter_spec.rb ├── jdbc_postgresql_adapter_spec.rb ├── mysql2_adapter_spec.rb ├── postgresql_adapter_spec.rb └── sqlite3_adapter_spec.rb ├── apartment_spec.rb ├── config └── database.yml.sample ├── dummy ├── Rakefile ├── app │ ├── controllers │ │ └── application_controller.rb │ ├── helpers │ │ └── application_helper.rb │ ├── models │ │ ├── company.rb │ │ └── user.rb │ └── views │ │ ├── application │ │ └── index.html.erb │ │ └── layouts │ │ └── application.html.erb ├── config.ru ├── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml.sample │ ├── environment.rb │ ├── environments │ │ ├── development.rb │ │ ├── production.rb │ │ └── test.rb │ ├── initializers │ │ ├── apartment.rb │ │ ├── backtrace_silencers.rb │ │ ├── inflections.rb │ │ ├── mime_types.rb │ │ ├── secret_token.rb │ │ └── session_store.rb │ ├── locales │ │ └── en.yml │ └── routes.rb ├── db │ ├── migrate │ │ ├── 20110613152810_create_dummy_models.rb │ │ ├── 20111202022214_create_table_books.rb │ │ └── 20180415260934_create_public_tokens.rb │ ├── schema.rb │ ├── seeds.rb │ ├── seeds │ │ └── import.rb │ └── test.sqlite3 ├── public │ ├── 404.html │ ├── 422.html │ ├── 500.html │ ├── favicon.ico │ └── stylesheets │ │ └── .gitkeep └── script │ └── rails ├── dummy_engine ├── .gitignore ├── Gemfile ├── Rakefile ├── bin │ └── rails ├── config │ └── initializers │ │ └── apartment.rb ├── dummy_engine.gemspec ├── lib │ ├── dummy_engine.rb │ └── dummy_engine │ │ ├── engine.rb │ │ └── version.rb └── test │ └── dummy │ ├── Rakefile │ ├── config.ru │ └── config │ ├── application.rb │ ├── boot.rb │ ├── database.yml │ ├── environment.rb │ ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb │ ├── initializers │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── session_store.rb │ └── wrap_parameters.rb │ ├── locales │ └── en.yml │ ├── routes.rb │ └── secrets.yml ├── examples ├── connection_adapter_examples.rb ├── generic_adapter_custom_configuration_example.rb ├── generic_adapter_examples.rb └── schema_adapter_examples.rb ├── integration ├── apartment_rake_integration_spec.rb ├── query_caching_spec.rb └── use_within_an_engine_spec.rb ├── schemas ├── v1.rb ├── v2.rb └── v3.rb ├── spec_helper.rb ├── support ├── apartment_helpers.rb ├── capybara_sessions.rb ├── config.rb ├── contexts.rb ├── requirements.rb └── setup.rb ├── tasks └── apartment_rake_spec.rb ├── tenant_spec.rb └── unit ├── config_spec.rb ├── elevators ├── domain_spec.rb ├── first_subdomain_spec.rb ├── generic_spec.rb ├── host_hash_spec.rb ├── host_spec.rb └── subdomain_spec.rb ├── migrator_spec.rb └── reloader_spec.rb /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Steps to reproduce 2 | 3 | ## Expected behavior 4 | 5 | ## Actual behavior 6 | 7 | ## System configuration 8 | 9 | 10 | 11 | * Database: (Tell us what database and its version you use.) 12 | 13 | * Apartment version: 14 | 15 | * Apartment config (in `config/initializers/apartment.rb` or so): 16 | 17 | * `use_schemas`: (`true` or `false`) 18 | 19 | * Rails (or ActiveRecord) version: 20 | 21 | * Ruby version: 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | *.lock 4 | gemfiles/*.lock 5 | gemfiles/vendor 6 | pkg/* 7 | *.log 8 | .idea 9 | *.sw[pno] 10 | spec/config/database.yml 11 | spec/dummy/config/database.yml 12 | cookbooks 13 | tmp 14 | spec/dummy/db/*.sqlite3 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.pryrc: -------------------------------------------------------------------------------- 1 | if defined?(Rails) && Rails.env 2 | extend Rails::ConsoleMethods 3 | end 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --colour 2 | --format documentation 3 | --tty 4 | --order random 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: ruby 3 | services: 4 | - docker 5 | rvm: 6 | - jruby-9.1.15.0 7 | - 2.1.9 8 | - 2.2.9 9 | - 2.3.6 10 | - 2.4.3 11 | - 2.5.0 12 | - 2.6.2 13 | - ruby-head 14 | gemfile: 15 | - gemfiles/rails_4_2.gemfile 16 | - gemfiles/rails_5_0.gemfile 17 | - gemfiles/rails_5_1.gemfile 18 | - gemfiles/rails_5_2.gemfile 19 | - gemfiles/rails_6_0.gemfile 20 | - gemfiles/rails_master.gemfile 21 | bundler_args: --without local 22 | before_install: 23 | - sudo /etc/init.d/mysql stop 24 | - sudo /etc/init.d/postgresql stop 25 | - docker-compose up -d 26 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true 27 | - gem uninstall bundler -v '>= 2' -x || true 28 | - gem install bundler -v '< 2' 29 | env: 30 | RUBY_GC_MALLOC_LIMIT: 90000000 31 | RUBY_GC_HEAP_FREE_SLOTS: 200000 32 | matrix: 33 | allow_failures: 34 | - rvm: ruby-head 35 | - gemfile: gemfiles/rails_master.gemfile 36 | - rvm: jruby-9.1.15.0 37 | gemfile: gemfiles/rails_5_0.gemfile 38 | exclude: 39 | - rvm: 2.1.9 40 | gemfile: gemfiles/rails_5_0.gemfile 41 | - rvm: 2.1.9 42 | gemfile: gemfiles/rails_5_1.gemfile 43 | - rvm: 2.1.9 44 | gemfile: gemfiles/rails_5_2.gemfile 45 | - rvm: 2.1.9 46 | gemfile: gemfiles/rails_6_0.gemfile 47 | - rvm: 2.1.9 48 | gemfile: gemfiles/rails_master.gemfile 49 | - rvm: 2.2.9 50 | gemfile: gemfiles/rails_6_0.gemfile 51 | - rvm: 2.3.6 52 | gemfile: gemfiles/rails_6_0.gemfile 53 | - rvm: 2.4.3 54 | gemfile: gemfiles/rails_6_0.gemfile 55 | - rvm: jruby-9.1.15.0 56 | gemfile: gemfiles/rails_5_1.gemfile 57 | - rvm: jruby-9.1.15.0 58 | gemfile: gemfiles/rails_5_2.gemfile 59 | - rvm: jruby-9.1.15.0 60 | gemfile: gemfiles/rails_6_0.gemfile 61 | - rvm: jruby-9.1.15.0 62 | gemfile: gemfiles/rails_master.gemfile 63 | fast_finish: true 64 | cache: bundler 65 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-4-2" do 2 | gem "rails", "~> 4.2.0" 3 | platforms :ruby do 4 | gem "pg", "< 1.0.0" 5 | gem "mysql2", "~> 0.4.0" 6 | end 7 | platforms :jruby do 8 | gem 'activerecord-jdbc-adapter', '~> 1.3' 9 | gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3' 10 | gem 'activerecord-jdbcmysql-adapter', '~> 1.3' 11 | end 12 | end 13 | 14 | appraise "rails-5-0" do 15 | gem "rails", "~> 5.0.0" 16 | platforms :ruby do 17 | gem "pg", "< 1.0.0" 18 | end 19 | platforms :jruby do 20 | gem 'activerecord-jdbc-adapter', '~> 50.0' 21 | gem 'activerecord-jdbcpostgresql-adapter', '~> 50.0' 22 | gem 'activerecord-jdbcmysql-adapter', '~> 50.0' 23 | end 24 | end 25 | 26 | appraise "rails-5-1" do 27 | gem "rails", "~> 5.1.0" 28 | platforms :ruby do 29 | gem "pg", "< 1.0.0" 30 | end 31 | platforms :jruby do 32 | gem 'activerecord-jdbc-adapter', '~> 51.0' 33 | gem 'activerecord-jdbcpostgresql-adapter', '~> 51.0' 34 | gem 'activerecord-jdbcmysql-adapter', '~> 51.0' 35 | end 36 | end 37 | 38 | appraise "rails-5-2" do 39 | gem "rails", "~> 5.2.0" 40 | platforms :jruby do 41 | gem 'activerecord-jdbc-adapter', '~> 52.0' 42 | gem 'activerecord-jdbcpostgresql-adapter', '~> 52.0' 43 | gem 'activerecord-jdbcmysql-adapter', '~> 52.0' 44 | end 45 | end 46 | 47 | 48 | appraise "rails-6-0" do 49 | gem "rails", "~> 6.0.0.rc1" 50 | platforms :ruby do 51 | gem 'sqlite3', '~> 1.4' 52 | end 53 | platforms :jruby do 54 | gem 'activerecord-jdbc-adapter', '~> 60.0.rc1' 55 | gem 'activerecord-jdbcpostgresql-adapter', '~> 60.0.rc1' 56 | gem 'activerecord-jdbcmysql-adapter', '~> 60.0.rc1' 57 | end 58 | end 59 | 60 | 61 | appraise "rails-master" do 62 | gem "rails", git: 'https://github.com/rails/rails.git' 63 | platforms :ruby do 64 | gem 'sqlite3', '~> 1.4' 65 | end 66 | platforms :jruby do 67 | gem 'activerecord-jdbc-adapter', '~> 52.0' 68 | gem 'activerecord-jdbcpostgresql-adapter', '~> 52.0' 69 | gem 'activerecord-jdbcmysql-adapter', '~> 52.0' 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec 4 | 5 | gem 'rails', '>= 3.1.2' 6 | 7 | group :local do 8 | gem 'pry' 9 | gem 'guard-rspec', '~> 4.2' 10 | end 11 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard :rspec do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch(%r{^lib/apartment/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } 7 | watch(%r{^lib/apartment/(.+)\.rb$}) { |m| "spec/integration/#{m[1]}_spec.rb" } 8 | watch('spec/spec_helper.rb') { "spec" } 9 | 10 | # # Rails example 11 | # watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 12 | # watch(%r{^app/(.*)(\.erb|\.haml|\.slim)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } 13 | # watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } 14 | # watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 15 | # watch('config/routes.rb') { "spec/routing" } 16 | # watch('app/controllers/application_controller.rb') { "spec/controllers" } 17 | 18 | # # Capybara features specs 19 | # watch(%r{^app/views/(.+)/.*\.(erb|haml|slim)$}) { |m| "spec/features/#{m[1]}_spec.rb" } 20 | 21 | # # Turnip features and steps 22 | # watch(%r{^spec/acceptance/(.+)\.feature$}) 23 | # watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 24 | end 25 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.2.1 2 | * June 19, 2019 3 | 4 | ## Added 5 | - #566: IGNORE_EMPTY_TENANTS environment variable to ignore empty tenants 6 | warning. [Pysis868] 7 | 8 | ## Fixed 9 | - #586: Ignore `CREATE SCHEMA public` statement in pg dump [artemave] 10 | - #549: Fix Postgres schema creation with dump SQL [ancorcruz] 11 | 12 | # 2.2.0 13 | * April 14, 2018 14 | 15 | ## Added 16 | - #523: Add Rails 5.2 support [IngusSkaistkalns] 17 | - #504: Test against Ruby 2.5.0 [ahorek] 18 | - #528: Test against Rails 5.2 [meganemura] 19 | 20 | ## Removed 21 | - #504: Remove Rails 4.0/4.1 support [ahorek] 22 | - #545: Stop supporting for JRuby + Rails 5.0 [meganemura] 23 | 24 | ## Fixed 25 | - #537: Fix PostgresqlSchemaFromSqlAdapter for newer PostgreSQL [shterrett] 26 | - #532: Issue is reported by [aldrinmartoq] 27 | - #519: Fix exception when main database doesn't exist [mayeco] 28 | 29 | ## Changed 30 | - #514: Fix typo [menorval] 31 | 32 | # 2.1.0 33 | * December 15, 2017 34 | 35 | - Add `parallel_migration_threads` configuration option for running migrations 36 | in parallel [ryanbrunner] 37 | - Drop Ruby 2.0.0 support [meganemura] 38 | - ignore_private when parsing subdomains with PublicSuffix [michiomochi] 39 | - Ignore row_security statements in psql dumps for backward compatibility 40 | [meganemura] 41 | - "Host" elevator [shrmnk] 42 | - Enhance db:drop task to act on all tenants [kuzukuzu] 43 | 44 | # 2.0.0 45 | * July 26, 2017 46 | 47 | - Raise FileNotFound rather than abort when loading files [meganemura] 48 | - Add 5.1 support with fixes for deprecations [meganemura] 49 | - Fix tests for 5.x and a host of dev-friendly improvements [meganemura] 50 | - Keep query cache config after switching databases [fernandomm] 51 | - Pass constants not strings to middleware stack (Rails 5) [tzabaman] 52 | - Remove deprecations from 1.0.0 [caironoleto] 53 | - Replace `tld_length` configuration option with PublicSuffix gem for the 54 | subdomain elevator [humancopy] 55 | - Pass full config to create_database to allow :encoding/:collation/etc 56 | [kakipo] 57 | - Don't retain a connection during initialization [mikecmpbll] 58 | - Fix database name escaping in drop_command [mikecmpbll] 59 | - Skip initialization for assets:clean and assets:precompile tasks 60 | [frank-west-iii] 61 | 62 | # 1.2.0 63 | * July 28, 2016 64 | 65 | - Official Rails 5 support 66 | 67 | # 1.1.0 68 | * May 26, 2016 69 | 70 | - Reset tenant after each request 71 | - [Support callbacks](https://github.com/influitive/apartment/commit/ff9c9d092a781026502f5997c0bbedcb5748bc83) on switch [cbeer] 72 | - Preliminary support for [separate database hosts](https://github.com/influitive/apartment/commit/abdffbf8cd9fba87243f16c86390da13e318ee1f) [apneadiving] 73 | 74 | # 1.0.2 75 | * July 2, 2015 76 | 77 | - Fix pg_dump env vars - pull/208 [MitinPavel] 78 | - Allow custom seed data file - pull/234 [typeoneerror] 79 | 80 | # 1.0.1 81 | * April 28, 2015 82 | 83 | - Fix `Apartment::Deprecation` which was rescuing all exceptions 84 | 85 | # 1.0.0 86 | * Feb 3, 2015 87 | 88 | - [BREAKING CHANGE] `Apartment::Tenant.process` is deprecated in favour of `Apartment::Tenant.switch` 89 | - [BREAKING CHANGE] `Apartment::Tenant.switch` without a block is deprecated in favour of `Apartment::Tenant.switch!` 90 | - Raise proper `TenantNotFound`, `TenantExists` exceptions 91 | - Deprecate old `SchemaNotFound`, `DatabaseNotFound` exceptions 92 | 93 | # 0.26.1 94 | * Jan 13, 2015 95 | 96 | - Fixed [schema quoting bug](https://github.com/influitive/apartment/issues/198#issuecomment-69782651) [jonsgreen] 97 | 98 | # 0.26.0 99 | * Jan 5, 2015 100 | 101 | - Rails 4.2 support 102 | 103 | # 0.25.2 104 | * Sept 8, 2014 105 | 106 | - Heroku fix on `assets:precompile` - pull/169 [rabbitt] 107 | 108 | # 0.25.1 109 | * July 17, 2014 110 | 111 | - Fixed a few vestiges of Apartment::Database 112 | 113 | # 0.25.0 114 | * July 3, 2014 115 | 116 | - [BREAKING CHANGE] - `Apartment::Database` is not deprecated in favour of 117 | `Apartment::Tenant` 118 | - ActiveRecord (and Rails) 4.1 now supported 119 | - A new sql based adapter that dumps the schema using sql 120 | 121 | # 0.24.3 122 | * March 5, 2014 123 | 124 | - Rake enhancements weren't removed from the generator template 125 | 126 | # 0.24.2 127 | * February 24, 2014 128 | 129 | - Better warnings if `apartment:migrate` is run 130 | 131 | # 0.24.1 132 | * February 21, 2014 133 | 134 | - requiring `apartment/tasks/enhancements` in an initializer doesn't work 135 | - One can disable tenant migrations using `Apartment.db_migrate_tenants = false` in the Rakefile 136 | 137 | # 0.24 138 | * February 21, 2014 (In honour of the Women's Gold Medal in Hockey at Sochi) 139 | 140 | - [BREAKING CHANGE] `apartment:migrate` task no longer depends on `db:migrate` 141 | - Instead, you can `require 'apartment/tasks/enhancements'` in your Apartment initializer 142 | - This will enhance `rake db:migrate` to also run `apartment:migrate` 143 | - You can now forget about ever running `apartment:migrate` again 144 | - Numerous deprecations for things referencing the word 'database' 145 | - This is an ongoing effort to completely replace 'database' with 'tenant' as a better abstraction 146 | - Note the obvious `Apartment::Database` still exists but will hopefully become `Apartment::Tenant` soon 147 | 148 | # 0.23.2 149 | * January 9, 2014 150 | 151 | - Increased visibility of #parse_database_name warning 152 | 153 | # 0.23.1 154 | * January 8, 2014 155 | 156 | - Schema adapters now initialize with default and persistent schemas 157 | - Deprecated Apartment::Elevators#parse_database_name 158 | 159 | # 0.23.0 160 | * August 21, 2013 161 | 162 | - Subdomain Elevator now allows for exclusions 163 | - Delayed::Job has been completely removed 164 | 165 | # 0.22.1 166 | * August 21, 2013 167 | 168 | - Fix bug where if your ruby process importing the database schema is run 169 | from a directory other than the app root, Apartment wouldn't know what 170 | schema_migrations to insert into the database (Rails only) 171 | 172 | # 0.22.0 173 | * June 9, 2013 174 | 175 | - Numerous bug fixes: 176 | - Mysql reset could connect to wrong database [eric88] 177 | - Postgresql schema names weren't quoted properly [gdott9] 178 | - Fixed error message on SchemaNotFound in `process` 179 | - HostHash elevator allows mapping host based on hash contents [gdott9] 180 | - Official Sidekiq support with the [apartment-sidekiq gem](https://github.com/influitive/apartment-sidekiq) 181 | 182 | 183 | # 0.21.1 184 | * May 31, 2013 185 | 186 | - Clearing the AR::QueryCache after switching databases. 187 | - Fixes issue with stale model being loaded for schema adapters 188 | 189 | # 0.21.0 190 | * April 24, 2013 191 | 192 | - JDBC support!! [PetrolMan] 193 | 194 | # 0.20.0 195 | * Feb 6, 2013 196 | 197 | - Mysql now has a 'schema like' option to perform like Postgresql (default) 198 | - This should be significantly more performant than using connections 199 | - Psych is now supported for Delayed::Job yaml parsing 200 | 201 | # 0.19.2 202 | * Jan 30, 2013 203 | 204 | - Database schema file can now be set manually or skipped altogether 205 | 206 | # 0.19.1 207 | * Jan 30, 2013 208 | 209 | - Allow schema.rb import file to be specified in config or skip schema.rb import altogether 210 | 211 | # 0.19.0 212 | * Dec 29, 2012 213 | 214 | - Apartment is now threadsafe 215 | - New postgis adapter [zonpantli] 216 | - Removed ActionDispatch dependency for use with Rack apps (regression) 217 | 218 | # 0.18.0 219 | * Nov 27, 2012 220 | 221 | - Added `append_environment` config option [virtualstaticvoid] 222 | - Cleaned up the readme and generator documentation 223 | - Added `connection_class` config option [smashtank] 224 | - Fixed a [bug](https://github.com/influitive/apartment/issues/17#issuecomment-10758327) in pg adapter when missing schema 225 | 226 | # 0.17.1 227 | * Oct 30, 2012 228 | 229 | - Fixed a bug where switching to an unknown db in mysql2 would crash the app [Frodotus] 230 | 231 | # 0.17.0 232 | * Sept 26, 2012 233 | 234 | - Apartment has [a new home!](https://github.com/influitive/apartment) 235 | - Support Sidekiq hooks to switch dbs [maedhr] 236 | - Allow VERSION to be used on apartment:migrate [Bhavin Kamani] 237 | 238 | # 0.16.0 239 | * June 1, 2012 240 | 241 | - Apartment now supports a default_schema to be set, rather than relying on ActiveRecord's default schema_search_path 242 | - Additional schemas can always be maintained in the schema_search_path by configuring persistent_schemas [ryanbrunner] 243 | - This means Hstore is officially supported!! 244 | - There is now a full domain based elevator to switch dbs based on the whole domain [lcowell] 245 | - There is now a generic elevator that takes a Proc to switch dbs based on the return value of that proc. 246 | 247 | # 0.15.0 248 | * March 18, 2012 249 | 250 | - Remove Rails dependency, Apartment can now be used with any Rack based framework using ActiveRecord 251 | 252 | # 0.14.4 253 | * March 8, 2012 254 | 255 | - Delayed::Job Hooks now return to the previous database, rather than resetting 256 | 257 | # 0.14.3 258 | * Feb 21, 2012 259 | 260 | - Fix yaml serialization of non DJ models 261 | 262 | # 0.14.2 263 | * Feb 21, 2012 264 | 265 | - Fix Delayed::Job yaml encoding with Rails > 3.0.x 266 | 267 | # 0.14.1 268 | * Dec 13, 2011 269 | 270 | - Fix ActionDispatch::Callbacks deprecation warnings 271 | 272 | # 0.14.0 273 | * Dec 13, 2011 274 | 275 | - Rails 3.1 Support 276 | 277 | # 0.13.1 278 | * Nov 8, 2011 279 | 280 | - Reset prepared statement cache for rails 3.1.1 before switching dbs when using postgresql schemas 281 | - Only necessary until the next release which will be more schema aware 282 | 283 | # 0.13.0 284 | * Oct 25, 2011 285 | 286 | - `process` will now rescue with reset if the previous schema/db is no longer available 287 | - `create` now takes an optional block which allows you to process within the newly created db 288 | - 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 289 | 290 | # 0.12.0 291 | * Oct 4, 2011 292 | 293 | - Added a `drop` method for removing databases/schemas 294 | - Refactored abstract adapter to further remove duplication in concrete implementations 295 | - Excluded models now take string references so they are properly reloaded in development 296 | - Better silencing of `schema.rb` loading using `verbose` flag 297 | 298 | # 0.11.1 299 | * Sep 22, 2011 300 | 301 | - Better use of Railties for initializing apartment 302 | - The following changes were necessary as I haven't figured out how to properly hook into Rails reloading 303 | - Added reloader middleware in development to init Apartment on each request 304 | - Override `reload!` in console to also init Apartment 305 | 306 | # 0.11.0 307 | * Sep 20, 2011 308 | 309 | - Excluded models no longer use a different connection when using postgresql schemas. Instead their table_name is prefixed with `public.` 310 | 311 | # 0.10.3 312 | * Sep 20, 2011 313 | 314 | - Fix improper raising of exceptions on create and reset 315 | 316 | # 0.10.2 317 | * Sep 15, 2011 318 | 319 | - Remove all the annoying logging for loading db schema and seeding on create 320 | 321 | # 0.10.1 322 | * Aug 11, 2011 323 | 324 | - Fixed bug in DJ where new objects (that hadn't been pulled from the db) didn't have the proper database assigned 325 | 326 | # 0.10.0 327 | * July 29, 2011 328 | 329 | - Added better support for Delayed Job 330 | - New config option that enables Delayed Job wrappers 331 | - 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 332 | 333 | # 0.9.2 334 | * July 4, 2011 335 | 336 | - Migrations now run associated rails migration fully, fixes schema.rb not being reloaded after migrations 337 | 338 | # 0.9.1 339 | * June 24, 2011 340 | 341 | - Hooks now take the payload object as an argument to fetch the proper db for DJ hooks 342 | 343 | # 0.9.0 344 | * June 23, 2011 345 | 346 | - Added module to provide delayed job hooks 347 | 348 | # 0.8.0 349 | * June 23, 2011 350 | 351 | - Added #current_database which will return the current database (or schema) name 352 | 353 | # 0.7.0 354 | * June 22, 2011 355 | 356 | - Added apartment:seed rake task for seeding all dbs 357 | 358 | # 0.6.0 359 | * June 21, 2011 360 | 361 | - Added #process to connect to new db, perform operations, then ensure a reset 362 | 363 | # 0.5.1 364 | * June 21, 2011 365 | 366 | - Fixed db migrate up/down/rollback 367 | - added db:redo 368 | 369 | # 0.5.0 370 | * June 20, 2011 371 | 372 | - Added the concept of an "Elevator", a rack based strategy for db switching 373 | - Added the Subdomain Elevator middleware to enabled db switching based on subdomain 374 | 375 | # 0.4.0 376 | * June 14, 2011 377 | 378 | - Added `configure` method on Apartment instead of using yml file, allows for dynamic setting of db names to migrate for rake task 379 | - Added `seed_after_create` config option to import seed data to new db on create 380 | 381 | # 0.3.0 382 | * June 10, 2011 383 | 384 | - Added full support for database migration 385 | - Added in method to establish new connection for excluded models on startup rather than on each switch 386 | 387 | # 0.2.0 388 | * June 6, 2011 * 389 | 390 | - Refactor to use more rails/active_support functionality 391 | - Refactor config to lazily load apartment.yml if exists 392 | - Remove OStruct and just use hashes for fetching methods 393 | - Added schema load on create instead of migrating from scratch 394 | 395 | # 0.1.3 396 | * March 30, 2011 * 397 | 398 | - Original pass from Ryan 399 | -------------------------------------------------------------------------------- /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 'appraisal' 6 | 7 | require "rspec" 8 | require "rspec/core/rake_task" 9 | 10 | RSpec::Core::RakeTask.new(:spec => %w{ db:copy_credentials db:test:prepare }) do |spec| 11 | spec.pattern = "spec/**/*_spec.rb" 12 | # spec.rspec_opts = '--order rand:47078' 13 | end 14 | 15 | namespace :spec do 16 | [:tasks, :unit, :adapters, :integration].each do |type| 17 | RSpec::Core::RakeTask.new(type => :spec) do |spec| 18 | spec.pattern = "spec/#{type}/**/*_spec.rb" 19 | end 20 | end 21 | end 22 | 23 | task :console do 24 | require 'pry' 25 | require 'apartment' 26 | ARGV.clear 27 | Pry.start 28 | end 29 | 30 | task :default => :spec 31 | 32 | namespace :db do 33 | namespace :test do 34 | task :prepare => %w{postgres:drop_db postgres:build_db mysql:drop_db mysql:build_db} 35 | end 36 | 37 | desc "copy sample database credential files over if real files don't exist" 38 | task :copy_credentials do 39 | require 'fileutils' 40 | apartment_db_file = 'spec/config/database.yml' 41 | rails_db_file = 'spec/dummy/config/database.yml' 42 | 43 | FileUtils.copy(apartment_db_file + '.sample', apartment_db_file, :verbose => true) unless File.exists?(apartment_db_file) 44 | FileUtils.copy(rails_db_file + '.sample', rails_db_file, :verbose => true) unless File.exists?(rails_db_file) 45 | end 46 | end 47 | 48 | namespace :postgres do 49 | require 'active_record' 50 | require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}" 51 | 52 | desc 'Build the PostgreSQL test databases' 53 | task :build_db do 54 | params = [] 55 | params << "-E UTF8" 56 | params << pg_config['database'] 57 | params << "-U#{pg_config['username']}" 58 | params << "-h#{pg_config['host']}" if pg_config['host'] 59 | params << "-p#{pg_config['port']}" if pg_config['port'] 60 | %x{ createdb #{params.join(' ')} } rescue "test db already exists" 61 | ActiveRecord::Base.establish_connection pg_config 62 | migrate 63 | end 64 | 65 | desc "drop the PostgreSQL test database" 66 | task :drop_db do 67 | puts "dropping database #{pg_config['database']}" 68 | params = [] 69 | params << pg_config['database'] 70 | params << "-U#{pg_config['username']}" 71 | params << "-h#{pg_config['host']}" if pg_config['host'] 72 | params << "-p#{pg_config['port']}" if pg_config['port'] 73 | %x{ dropdb #{params.join(' ')} } 74 | end 75 | 76 | end 77 | 78 | namespace :mysql do 79 | require 'active_record' 80 | require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}" 81 | 82 | desc 'Build the MySQL test databases' 83 | task :build_db do 84 | params = [] 85 | params << "-h #{my_config['host']}" if my_config['host'] 86 | params << "-u #{my_config['username']}" if my_config['username'] 87 | params << "-p#{my_config['password']}" if my_config['password'] 88 | %x{ mysqladmin #{params.join(' ')} create #{my_config['database']} } rescue "test db already exists" 89 | ActiveRecord::Base.establish_connection my_config 90 | migrate 91 | end 92 | 93 | desc "drop the MySQL test database" 94 | task :drop_db do 95 | puts "dropping database #{my_config['database']}" 96 | params = [] 97 | params << "-h #{my_config['host']}" if my_config['host'] 98 | params << "-u #{my_config['username']}" if my_config['username'] 99 | params << "-p#{my_config['password']}" if my_config['password'] 100 | %x{ mysqladmin #{params.join(' ')} drop #{my_config['database']} --force} 101 | end 102 | 103 | end 104 | 105 | # TODO clean this up 106 | def config 107 | Apartment::Test.config['connections'] 108 | end 109 | 110 | def pg_config 111 | config['postgresql'] 112 | end 113 | 114 | def my_config 115 | config['mysql'] 116 | end 117 | 118 | def activerecord_below_5_2? 119 | ActiveRecord.version.release < Gem::Version.new('5.2.0') 120 | end 121 | 122 | def migrate 123 | if activerecord_below_5_2? 124 | ActiveRecord::Migrator.migrate('spec/dummy/db/migrate') 125 | else 126 | ActiveRecord::MigrationContext.new('spec/dummy/db/migrate').migrate 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Apartment TODOs 2 | 3 | ### Below is a list of tasks in the approximate order to be completed of for Apartment 4 | ### Any help along the way is greatly appreciated (on any items, not particularly in order) 5 | 6 | 1. Apartment was originally written (and TDD'd) with just Postgresql in mind. Different adapters were added at a later date. 7 | As such, the test suite is a bit of a mess. There's no formal structure for fully integration testing all adapters to ensure 8 | proper quality and prevent regressions. 9 | 10 | There's also a test order dependency as some tests run assuming a db connection and if that test randomly ran before a previous 11 | one that makes the connection, it would fail. 12 | 13 | I'm proposing the first thing to be done is to write up a standard, high livel integration test case that can be applied to all adapters 14 | and makes no assumptions about implementation. It should ensure that each adapter conforms to the Apartment Interface and CRUD's properly. 15 | It would be nice if a user can 'register' an adapter such that it would automatically be tested (nice to have). Otherwise one could just use 16 | a shared behaviour to run through all of this. 17 | 18 | Then, I'd like to see all of the implementation specific tests just in their own test file for each adapter (ie the postgresql schema adapter checks a lot of things with `schema_search_path`) 19 | 20 | This should ensure that going forward nothing breaks, and we should *ideally* be able to randomize the test order 21 | 22 | 2. `Apartment::Database` is the wrong abstraction. When dealing with a multi-tenanted system, users shouldn't thing about 'Databases', they should 23 | think about Tenants. I proprose that we deprecate the `Apartment::Database` constant in favour of `Apartment::Tenant` for a nicer abstraction. See 24 | http://myronmars.to/n/dev-blog/2011/09/deprecating-constants-and-classes-in-ruby for ideas on how to achieve this. 25 | 26 | 4. Apartment::Database.process should be deprecated in favour of just passing a block to `switch` 27 | 5. Apartment::Database.switch should be renamed to switch! to indicate that using it on its own has side effects 28 | 29 | 6. Migrations right now can be a bit of a pain. Apartment currently migrates a single tenant completely up to date, then goes onto the next. If one of these 30 | migrations fails on a tenant, the previous one does NOT get reverted and leaves you in an awkward state. Ideally we'd want to wrap all of the migrations in 31 | a transaction so if one fails, the whole thing reverts. Once we can ensure an all-or-nothing approach to migrations, we can optimize the migration strategy 32 | to not even iterate over the tenants if there are no migrations to run on public. 33 | 34 | 7. Apartment has be come one of the most popular/robust Multi-tenant gems for Rails, but it still doesn't work for everyone's use case. It's fairly limited in implementation to either schema based (ie postgresql schemas) or connection based. I'd like to abstract out these implementation details such that one could write a pluggable strategy for Apartment and choose it based on a config selection (something like `config.strategy = :schema`). The next implementation I'd like to see is a scoped based approach that uses a `tenant_id` scoping on all records for multi-tenancy. This is probably the most popular multi-tenant approach and is db independent and really the simplest mechanism for a type of multi-tenancy. 35 | 36 | 8. Right now excluded tables still live in all tenanted environments. This is basically because it doesn't matter if they're there, we always query from the public. 37 | It's a bit of an annoyance though and confuses lots of people. I'd love to see only tenanted tables in the tenants and only excluded tables in the public tenant. 38 | This will be hard because Rails uses public to generate schema.rb. One idea is to have an `excluded` schema that holds all the excluded models and the public can 39 | maintain everything. 40 | 41 | 9. This one is pretty lofty, but I'd also like to abstract out the fact that Apartment uses ActiveRecord. With the new DataMapper coming out soon and other popular 42 | DBMS's (ie. mongo, couch etc...), it'd be nice if Apartment could be the de-facto interface for multi-tenancy on these systems. 43 | 44 | 45 | =================== 46 | 47 | Quick TODOs 48 | 49 | 1. `default_tenant` should be up to the adapter, not the Apartment class, deprecate `default_schema` 50 | 2. deprecation.rb rescues everything, we have a hard dependency on ActiveSupport so this is unnecessary 51 | 3. 52 | -------------------------------------------------------------------------------- /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} 11 | s.description = %q{Apartment allows Rack applications to deal with database multitenancy through ActiveRecord} 12 | s.email = ["ryan@influitive.com", "brad@influitive.com"] 13 | s.files = `git ls-files`.split($/) 14 | s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 15 | s.test_files = s.files.grep(%r{^(test|spec|features)/}) 16 | s.require_paths = ["lib"] 17 | 18 | s.homepage = %q{https://github.com/influitive/apartment} 19 | s.licenses = ["MIT"] 20 | 21 | # must be >= 3.1.2 due to bug in prepared_statements 22 | s.add_dependency 'activerecord', '>= 3.1.2', '< 6.1' 23 | s.add_dependency 'rack', '>= 1.3.6' 24 | s.add_dependency 'public_suffix', '>= 2' 25 | s.add_dependency 'parallel', '>= 0.7.1' 26 | 27 | s.add_development_dependency 'appraisal' 28 | s.add_development_dependency 'rake', '~> 0.9' 29 | s.add_development_dependency 'rspec', '~> 3.4' 30 | s.add_development_dependency 'rspec-rails', '~> 3.4' 31 | s.add_development_dependency 'capybara', '~> 2.0' 32 | s.add_development_dependency 'bundler', '>= 1.3', '< 2.0' 33 | 34 | if defined?(JRUBY_VERSION) 35 | s.add_development_dependency 'activerecord-jdbc-adapter' 36 | s.add_development_dependency 'activerecord-jdbcpostgresql-adapter' 37 | s.add_development_dependency 'activerecord-jdbcmysql-adapter' 38 | s.add_development_dependency 'jdbc-postgres' 39 | s.add_development_dependency 'jdbc-mysql' 40 | s.add_development_dependency 'jruby-openssl' 41 | else 42 | s.add_development_dependency 'mysql2' 43 | s.add_development_dependency 'pg' 44 | s.add_development_dependency 'sqlite3', '~> 1.3.6' 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | services: 3 | postgresql: 4 | image: postgres:9.5.12 5 | environment: 6 | POSTGRES_PASSWORD: "" 7 | ports: 8 | - "5432:5432" 9 | healthcheck: 10 | test: pg_isready -U postgres 11 | start_period: 10s 12 | interval: 10s 13 | timeout: 30s 14 | retries: 3 15 | mysql: 16 | image: mysql:5.7 17 | environment: 18 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 19 | ports: 20 | - "3306:3306" 21 | healthcheck: 22 | test: mysqladmin -h 127.0.0.1 -uroot ping 23 | start_period: 15s 24 | interval: 10s 25 | timeout: 30s 26 | retries: 3 27 | healthcheck: 28 | image: busybox 29 | depends_on: 30 | postgresql: 31 | condition: service_healthy 32 | mysql: 33 | condition: service_healthy 34 | -------------------------------------------------------------------------------- /gemfiles/rails_4_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 4.2.0" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :ruby do 13 | gem "pg", "< 1.0.0" 14 | gem "mysql2", "~> 0.4.0" 15 | end 16 | 17 | platforms :jruby do 18 | gem "activerecord-jdbc-adapter", "~> 1.3" 19 | gem "activerecord-jdbcpostgresql-adapter", "~> 1.3" 20 | gem "activerecord-jdbcmysql-adapter", "~> 1.3" 21 | end 22 | 23 | gemspec path: "../" 24 | -------------------------------------------------------------------------------- /gemfiles/rails_5_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.0.0" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :ruby do 13 | gem "pg", "< 1.0.0" 14 | end 15 | 16 | platforms :jruby do 17 | gem "activerecord-jdbc-adapter", "~> 50.0" 18 | gem "activerecord-jdbcpostgresql-adapter", "~> 50.0" 19 | gem "activerecord-jdbcmysql-adapter", "~> 50.0" 20 | end 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_5_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.1.0" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :ruby do 13 | gem "pg", "< 1.0.0" 14 | end 15 | 16 | platforms :jruby do 17 | gem "activerecord-jdbc-adapter", "~> 51.0" 18 | gem "activerecord-jdbcpostgresql-adapter", "~> 51.0" 19 | gem "activerecord-jdbcmysql-adapter", "~> 51.0" 20 | end 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_5_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 5.2.0" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :jruby do 13 | gem "activerecord-jdbc-adapter", "~> 52.0" 14 | gem "activerecord-jdbcpostgresql-adapter", "~> 52.0" 15 | gem "activerecord-jdbcmysql-adapter", "~> 52.0" 16 | end 17 | 18 | gemspec path: "../" 19 | -------------------------------------------------------------------------------- /gemfiles/rails_6_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "~> 6.0.0.rc1" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :ruby do 13 | gem "sqlite3", "~> 1.4" 14 | end 15 | 16 | platforms :jruby do 17 | gem "activerecord-jdbc-adapter", "~> 60.0.rc1" 18 | gem "activerecord-jdbcpostgresql-adapter", "~> 60.0.rc1" 19 | gem "activerecord-jdbcmysql-adapter", "~> 60.0.rc1" 20 | end 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /gemfiles/rails_master.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", git: "https://github.com/rails/rails.git" 6 | 7 | group :local do 8 | gem "pry" 9 | gem "guard-rspec", "~> 4.2" 10 | end 11 | 12 | platforms :ruby do 13 | gem "sqlite3", "~> 1.4" 14 | end 15 | 16 | platforms :jruby do 17 | gem "activerecord-jdbc-adapter", "~> 52.0" 18 | gem "activerecord-jdbcpostgresql-adapter", "~> 52.0" 19 | gem "activerecord-jdbcmysql-adapter", "~> 52.0" 20 | end 21 | 22 | gemspec path: "../" 23 | -------------------------------------------------------------------------------- /lib/apartment.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/railtie' if defined?(Rails) 2 | require 'active_support/core_ext/object/blank' 3 | require 'forwardable' 4 | require 'active_record' 5 | require 'apartment/tenant' 6 | 7 | module Apartment 8 | 9 | class << self 10 | 11 | extend Forwardable 12 | 13 | ACCESSOR_METHODS = [:use_schemas, :use_sql, :seed_after_create, :prepend_environment, :append_environment, :with_multi_server_setup] 14 | WRITER_METHODS = [:tenant_names, :database_schema_file, :excluded_models, :default_schema, :persistent_schemas, :connection_class, :tld_length, :db_migrate_tenants, :seed_data_file, :parallel_migration_threads, :pg_excluded_names] 15 | 16 | attr_accessor(*ACCESSOR_METHODS) 17 | attr_writer(*WRITER_METHODS) 18 | 19 | def_delegators :connection_class, :connection, :connection_config, :establish_connection 20 | 21 | # configure apartment with available options 22 | def configure 23 | yield self if block_given? 24 | end 25 | 26 | def tenant_names 27 | extract_tenant_config.keys.map(&:to_s) 28 | end 29 | 30 | def tenants_with_config 31 | extract_tenant_config 32 | end 33 | 34 | def db_config_for(tenant) 35 | (tenants_with_config[tenant] || connection_config).with_indifferent_access 36 | end 37 | 38 | # Whether or not db:migrate should also migrate tenants 39 | # defaults to true 40 | def db_migrate_tenants 41 | return @db_migrate_tenants if defined?(@db_migrate_tenants) 42 | 43 | @db_migrate_tenants = true 44 | end 45 | 46 | # Default to empty array 47 | def excluded_models 48 | @excluded_models || [] 49 | end 50 | 51 | def default_schema 52 | @default_schema || "public" # TODO 'public' is postgres specific 53 | end 54 | 55 | def parallel_migration_threads 56 | @parallel_migration_threads || 0 57 | end 58 | alias :default_tenant :default_schema 59 | alias :default_tenant= :default_schema= 60 | 61 | def persistent_schemas 62 | @persistent_schemas || [] 63 | end 64 | 65 | def connection_class 66 | @connection_class || ActiveRecord::Base 67 | end 68 | 69 | def database_schema_file 70 | return @database_schema_file if defined?(@database_schema_file) 71 | 72 | @database_schema_file = Rails.root.join('db', 'schema.rb') 73 | end 74 | 75 | def seed_data_file 76 | return @seed_data_file if defined?(@seed_data_file) 77 | 78 | @seed_data_file = "#{Rails.root}/db/seeds.rb" 79 | end 80 | 81 | def pg_excluded_names 82 | @pg_excluded_names || [] 83 | end 84 | 85 | # Reset all the config for Apartment 86 | def reset 87 | (ACCESSOR_METHODS + WRITER_METHODS).each{|method| remove_instance_variable(:"@#{method}") if instance_variable_defined?(:"@#{method}") } 88 | end 89 | 90 | def extract_tenant_config 91 | return {} unless @tenant_names 92 | values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names 93 | unless values.is_a? Hash 94 | values = values.each_with_object({}) do |tenant, hash| 95 | hash[tenant] = connection_config 96 | end 97 | end 98 | values.with_indifferent_access 99 | rescue ActiveRecord::StatementInvalid 100 | {} 101 | end 102 | end 103 | 104 | # Exceptions 105 | ApartmentError = Class.new(StandardError) 106 | 107 | # Raised when apartment cannot find the adapter specified in config/database.yml 108 | AdapterNotFound = Class.new(ApartmentError) 109 | 110 | # Raised when apartment cannot find the file to be loaded 111 | FileNotFound = Class.new(ApartmentError) 112 | 113 | # Tenant specified is unknown 114 | TenantNotFound = Class.new(ApartmentError) 115 | 116 | # The Tenant attempting to be created already exists 117 | TenantExists = Class.new(ApartmentError) 118 | end 119 | -------------------------------------------------------------------------------- /lib/apartment/adapters/abstract_adapter.rb: -------------------------------------------------------------------------------- 1 | module Apartment 2 | module Adapters 3 | class AbstractAdapter 4 | include ActiveSupport::Callbacks 5 | define_callbacks :create, :switch 6 | 7 | attr_writer :default_tenant 8 | 9 | # @constructor 10 | # @param {Hash} config Database config 11 | # 12 | def initialize(config) 13 | @config = config 14 | end 15 | 16 | # Create a new tenant, import schema, seed if appropriate 17 | # 18 | # @param {String} tenant Tenant name 19 | # 20 | def create(tenant) 21 | run_callbacks :create do 22 | create_tenant(tenant) 23 | 24 | switch(tenant) do 25 | import_database_schema 26 | 27 | # Seed data if appropriate 28 | seed_data if Apartment.seed_after_create 29 | 30 | yield if block_given? 31 | end 32 | end 33 | end 34 | 35 | # Note alias_method here doesn't work with inheritence apparently ?? 36 | # 37 | def current 38 | Apartment.connection.current_database 39 | end 40 | 41 | # Return the original public tenant 42 | # 43 | # @return {String} default tenant name 44 | # 45 | def default_tenant 46 | @default_tenant || Apartment.default_tenant 47 | end 48 | alias :default_schema :default_tenant # TODO deprecate default_schema 49 | 50 | # Drop the tenant 51 | # 52 | # @param {String} tenant name 53 | # 54 | def drop(tenant) 55 | with_neutral_connection(tenant) do |conn| 56 | drop_command(conn, tenant) 57 | end 58 | 59 | rescue *rescuable_exceptions => exception 60 | raise_drop_tenant_error!(tenant, exception) 61 | end 62 | 63 | # Switch to a new tenant 64 | # 65 | # @param {String} tenant name 66 | # 67 | def switch!(tenant = nil) 68 | run_callbacks :switch do 69 | return reset if tenant.nil? 70 | 71 | connect_to_new(tenant).tap do 72 | Apartment.connection.clear_query_cache 73 | end 74 | end 75 | end 76 | 77 | # Connect to tenant, do your biz, switch back to previous tenant 78 | # 79 | # @param {String?} tenant to connect to 80 | # 81 | def switch(tenant = nil) 82 | begin 83 | previous_tenant = current 84 | switch!(tenant) 85 | yield 86 | 87 | ensure 88 | switch!(previous_tenant) rescue reset 89 | end 90 | end 91 | 92 | # Iterate over all tenants, switch to tenant and yield tenant name 93 | # 94 | def each(tenants = Apartment.tenant_names) 95 | tenants.each do |tenant| 96 | switch(tenant){ yield tenant } 97 | end 98 | end 99 | 100 | # Establish a new connection for each specific excluded model 101 | # 102 | def process_excluded_models 103 | # All other models will shared a connection (at Apartment.connection_class) and we can modify at will 104 | Apartment.excluded_models.each do |excluded_model| 105 | process_excluded_model(excluded_model) 106 | end 107 | end 108 | 109 | # Reset the tenant connection to the default 110 | # 111 | def reset 112 | Apartment.establish_connection @config 113 | end 114 | 115 | # Load the rails seed file into the db 116 | # 117 | def seed_data 118 | # Don't log the output of seeding the db 119 | silence_warnings{ load_or_raise(Apartment.seed_data_file) } if Apartment.seed_data_file 120 | end 121 | alias_method :seed, :seed_data 122 | 123 | # Prepend the environment if configured and the environment isn't already there 124 | # 125 | # @param {String} tenant Database name 126 | # @return {String} tenant name with Rails environment *optionally* prepended 127 | # 128 | def environmentify(tenant) 129 | unless tenant.include?(Rails.env) 130 | if Apartment.prepend_environment 131 | "#{Rails.env}_#{tenant}" 132 | elsif Apartment.append_environment 133 | "#{tenant}_#{Rails.env}" 134 | else 135 | tenant 136 | end 137 | else 138 | tenant 139 | end 140 | end 141 | 142 | protected 143 | 144 | def process_excluded_model(excluded_model) 145 | excluded_model.constantize.establish_connection @config 146 | end 147 | 148 | def drop_command(conn, tenant) 149 | # connection.drop_database note that drop_database will not throw an exception, so manually execute 150 | conn.execute("DROP DATABASE #{conn.quote_table_name(environmentify(tenant))}") 151 | end 152 | 153 | # Create the tenant 154 | # 155 | # @param {String} tenant Database name 156 | # 157 | def create_tenant(tenant) 158 | with_neutral_connection(tenant) do |conn| 159 | create_tenant_command(conn, tenant) 160 | end 161 | rescue *rescuable_exceptions => exception 162 | raise_create_tenant_error!(tenant, exception) 163 | end 164 | 165 | def create_tenant_command(conn, tenant) 166 | conn.create_database(environmentify(tenant), @config) 167 | end 168 | 169 | # Connect to new tenant 170 | # 171 | # @param {String} tenant Database name 172 | # 173 | def connect_to_new(tenant) 174 | query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled 175 | 176 | Apartment.establish_connection multi_tenantify(tenant) 177 | Apartment.connection.active? # call active? to manually check if this connection is valid 178 | 179 | Apartment.connection.enable_query_cache! if query_cache_enabled 180 | rescue *rescuable_exceptions => exception 181 | Apartment::Tenant.reset if reset_on_connection_exception? 182 | raise_connect_error!(tenant, exception) 183 | end 184 | 185 | # Import the database schema 186 | # 187 | def import_database_schema 188 | ActiveRecord::Schema.verbose = false # do not log schema load output. 189 | 190 | load_or_raise(Apartment.database_schema_file) if Apartment.database_schema_file 191 | end 192 | 193 | # Return a new config that is multi-tenanted 194 | # @param {String} tenant: Database name 195 | # @param {Boolean} with_database: if true, use the actual tenant's db name 196 | # if false, use the default db name from the db 197 | def multi_tenantify(tenant, with_database = true) 198 | db_connection_config(tenant).tap do |config| 199 | if with_database 200 | multi_tenantify_with_tenant_db_name(config, tenant) 201 | end 202 | end 203 | end 204 | 205 | def multi_tenantify_with_tenant_db_name(config, tenant) 206 | config[:database] = environmentify(tenant) 207 | end 208 | 209 | # Load a file or raise error if it doesn't exists 210 | # 211 | def load_or_raise(file) 212 | if File.exists?(file) 213 | load(file) 214 | else 215 | raise FileNotFound, "#{file} doesn't exist yet" 216 | end 217 | end 218 | # Backward compatibility 219 | alias_method :load_or_abort, :load_or_raise 220 | 221 | # Exceptions to rescue from on db operations 222 | # 223 | def rescuable_exceptions 224 | [ActiveRecord::ActiveRecordError] + Array(rescue_from) 225 | end 226 | 227 | # Extra exceptions to rescue from 228 | # 229 | def rescue_from 230 | [] 231 | end 232 | 233 | def db_connection_config(tenant) 234 | Apartment.db_config_for(tenant).clone 235 | end 236 | 237 | def with_neutral_connection(tenant, &block) 238 | if Apartment.with_multi_server_setup 239 | # neutral connection is necessary whenever you need to create/remove a database from a server. 240 | # example: when you use postgresql, you need to connect to the default postgresql database before you create your own. 241 | SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false)) 242 | yield(SeparateDbConnectionHandler.connection) 243 | SeparateDbConnectionHandler.connection.close 244 | else 245 | yield(Apartment.connection) 246 | end 247 | end 248 | 249 | def reset_on_connection_exception? 250 | false 251 | end 252 | 253 | def raise_drop_tenant_error!(tenant, exception) 254 | raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{ exception.message }" 255 | end 256 | 257 | def raise_create_tenant_error!(tenant, exception) 258 | raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{ exception.message }" 259 | end 260 | 261 | def raise_connect_error!(tenant, exception) 262 | raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{ exception.message }" 263 | end 264 | 265 | class SeparateDbConnectionHandler < ::ActiveRecord::Base 266 | end 267 | end 268 | end 269 | end 270 | -------------------------------------------------------------------------------- /lib/apartment/adapters/abstract_jdbc_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/adapters/abstract_adapter' 2 | 3 | module Apartment 4 | module Adapters 5 | class AbstractJDBCAdapter < AbstractAdapter 6 | 7 | private 8 | 9 | def multi_tenantify_with_tenant_db_name(config, tenant) 10 | config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}" 11 | end 12 | 13 | def rescue_from 14 | ActiveRecord::JDBCError 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/apartment/adapters/jdbc_mysql_adapter.rb: -------------------------------------------------------------------------------- 1 | require "apartment/adapters/abstract_jdbc_adapter" 2 | 3 | module Apartment 4 | 5 | module Tenant 6 | def self.jdbc_mysql_adapter(config) 7 | Adapters::JDBCMysqlAdapter.new config 8 | end 9 | end 10 | 11 | module Adapters 12 | class JDBCMysqlAdapter < AbstractJDBCAdapter 13 | 14 | def reset_on_connection_exception? 15 | true 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/apartment/adapters/jdbc_postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/adapters/postgresql_adapter' 2 | 3 | module Apartment 4 | module Tenant 5 | 6 | def self.jdbc_postgresql_adapter(config) 7 | Apartment.use_schemas ? 8 | Adapters::JDBCPostgresqlSchemaAdapter.new(config) : 9 | Adapters::JDBCPostgresqlAdapter.new(config) 10 | end 11 | end 12 | 13 | module Adapters 14 | 15 | # Default adapter when not using Postgresql Schemas 16 | class JDBCPostgresqlAdapter < PostgresqlAdapter 17 | 18 | private 19 | 20 | def multi_tenantify_with_tenant_db_name(config, tenant) 21 | config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}" 22 | end 23 | 24 | def create_tenant_command(conn, tenant) 25 | conn.create_database(environmentify(tenant), { :thisisahack => '' }) 26 | end 27 | 28 | def rescue_from 29 | ActiveRecord::JDBCError 30 | end 31 | end 32 | 33 | # Separate Adapter for Postgresql when using schemas 34 | class JDBCPostgresqlSchemaAdapter < PostgresqlSchemaAdapter 35 | 36 | # Set schema search path to new schema 37 | # 38 | def connect_to_new(tenant = nil) 39 | return reset if tenant.nil? 40 | raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.all_schemas.include? tenant.to_s 41 | 42 | @current = tenant.to_s 43 | Apartment.connection.schema_search_path = full_search_path 44 | 45 | rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError 46 | raise TenantNotFound, "One of the following schema(s) is invalid: #{full_search_path}" 47 | end 48 | 49 | private 50 | 51 | def rescue_from 52 | ActiveRecord::JDBCError 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/apartment/adapters/mysql2_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/adapters/abstract_adapter' 2 | 3 | module Apartment 4 | module Tenant 5 | 6 | def self.mysql2_adapter(config) 7 | Apartment.use_schemas ? 8 | Adapters::Mysql2SchemaAdapter.new(config) : 9 | Adapters::Mysql2Adapter.new(config) 10 | end 11 | end 12 | 13 | module Adapters 14 | class Mysql2Adapter < AbstractAdapter 15 | 16 | def initialize(config) 17 | super 18 | 19 | @default_tenant = config[:database] 20 | end 21 | 22 | protected 23 | 24 | def rescue_from 25 | Mysql2::Error 26 | end 27 | end 28 | 29 | class Mysql2SchemaAdapter < AbstractAdapter 30 | def initialize(config) 31 | super 32 | 33 | @default_tenant = config[:database] 34 | reset 35 | end 36 | 37 | # Reset current tenant to the default_tenant 38 | # 39 | def reset 40 | Apartment.connection.execute "use `#{default_tenant}`" 41 | end 42 | 43 | protected 44 | 45 | # Connect to new tenant 46 | # 47 | def connect_to_new(tenant) 48 | return reset if tenant.nil? 49 | 50 | Apartment.connection.execute "use `#{environmentify(tenant)}`" 51 | 52 | rescue ActiveRecord::StatementInvalid => exception 53 | Apartment::Tenant.reset 54 | raise_connect_error!(tenant, exception) 55 | end 56 | 57 | def process_excluded_model(model) 58 | model.constantize.tap do |klass| 59 | # Ensure that if a schema *was* set, we override 60 | table_name = klass.table_name.split('.', 2).last 61 | 62 | klass.table_name = "#{default_tenant}.#{table_name}" 63 | end 64 | end 65 | 66 | def reset_on_connection_exception? 67 | true 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/apartment/adapters/postgis_adapter.rb: -------------------------------------------------------------------------------- 1 | # handle postgis adapter as if it were postgresql, 2 | # only override the adapter_method used for initialization 3 | require "apartment/adapters/postgresql_adapter" 4 | 5 | module Apartment 6 | module Tenant 7 | 8 | def self.postgis_adapter(config) 9 | postgresql_adapter(config) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/apartment/adapters/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/adapters/abstract_adapter' 2 | 3 | module Apartment 4 | module Tenant 5 | 6 | def self.postgresql_adapter(config) 7 | adapter = Adapters::PostgresqlAdapter 8 | adapter = Adapters::PostgresqlSchemaAdapter if Apartment.use_schemas 9 | adapter = Adapters::PostgresqlSchemaFromSqlAdapter if Apartment.use_sql && Apartment.use_schemas 10 | adapter.new(config) 11 | end 12 | end 13 | 14 | module Adapters 15 | # Default adapter when not using Postgresql Schemas 16 | class PostgresqlAdapter < AbstractAdapter 17 | 18 | private 19 | 20 | def rescue_from 21 | PG::Error 22 | end 23 | end 24 | 25 | # Separate Adapter for Postgresql when using schemas 26 | class PostgresqlSchemaAdapter < AbstractAdapter 27 | 28 | def initialize(config) 29 | super 30 | 31 | reset 32 | end 33 | 34 | # Reset schema search path to the default schema_search_path 35 | # 36 | # @return {String} default schema search path 37 | # 38 | def reset 39 | @current = default_tenant 40 | Apartment.connection.schema_search_path = full_search_path 41 | end 42 | 43 | def current 44 | @current || default_tenant 45 | end 46 | 47 | protected 48 | 49 | def process_excluded_model(excluded_model) 50 | excluded_model.constantize.tap do |klass| 51 | # Ensure that if a schema *was* set, we override 52 | table_name = klass.table_name.split('.', 2).last 53 | 54 | klass.table_name = "#{default_tenant}.#{table_name}" 55 | end 56 | end 57 | 58 | def drop_command(conn, tenant) 59 | conn.execute(%{DROP SCHEMA "#{tenant}" CASCADE}) 60 | end 61 | 62 | # Set schema search path to new schema 63 | # 64 | def connect_to_new(tenant = nil) 65 | return reset if tenant.nil? 66 | raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.schema_exists?(tenant.to_s) 67 | 68 | @current = tenant.to_s 69 | Apartment.connection.schema_search_path = full_search_path 70 | 71 | # When the PostgreSQL version is < 9.3, 72 | # there is a issue for prepared statement with changing search_path. 73 | # https://www.postgresql.org/docs/9.3/static/sql-prepare.html 74 | if postgresql_version < 90300 75 | Apartment.connection.clear_cache! 76 | end 77 | 78 | rescue *rescuable_exceptions 79 | raise TenantNotFound, "One of the following schema(s) is invalid: \"#{tenant}\" #{full_search_path}" 80 | end 81 | 82 | private 83 | 84 | def create_tenant_command(conn, tenant) 85 | conn.execute(%{CREATE SCHEMA "#{tenant}"}) 86 | end 87 | 88 | # Generate the final search path to set including persistent_schemas 89 | # 90 | def full_search_path 91 | persistent_schemas.map(&:inspect).join(", ") 92 | end 93 | 94 | def persistent_schemas 95 | [@current, Apartment.persistent_schemas].flatten 96 | end 97 | 98 | def postgresql_version 99 | # ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#postgresql_version is 100 | # public from Rails 5.0. 101 | Apartment.connection.send(:postgresql_version) 102 | end 103 | end 104 | 105 | # Another Adapter for Postgresql when using schemas and SQL 106 | class PostgresqlSchemaFromSqlAdapter < PostgresqlSchemaAdapter 107 | 108 | PSQL_DUMP_BLACKLISTED_STATEMENTS= [ 109 | /SET search_path/i, # overridden later 110 | /SET lock_timeout/i, # new in postgresql 9.3 111 | /SET row_security/i, # new in postgresql 9.5 112 | /SET idle_in_transaction_session_timeout/i, # new in postgresql 9.6 113 | /CREATE SCHEMA public/i, 114 | /COMMENT ON SCHEMA public/i, 115 | 116 | ] 117 | 118 | def import_database_schema 119 | preserving_search_path do 120 | clone_pg_schema 121 | copy_schema_migrations 122 | end 123 | end 124 | 125 | private 126 | 127 | # Re-set search path after the schema is imported. 128 | # Postgres now sets search path to empty before dumping the schema 129 | # and it mut be reset 130 | # 131 | def preserving_search_path 132 | search_path = Apartment.connection.execute("show search_path").first["search_path"] 133 | yield 134 | Apartment.connection.execute("set search_path = #{search_path}") 135 | end 136 | 137 | # Clone default schema into new schema named after current tenant 138 | # 139 | def clone_pg_schema 140 | pg_schema_sql = patch_search_path(pg_dump_schema) 141 | Apartment.connection.execute(pg_schema_sql) 142 | end 143 | 144 | # Copy data from schema_migrations into new schema 145 | # 146 | def copy_schema_migrations 147 | pg_migrations_data = patch_search_path(pg_dump_schema_migrations_data) 148 | Apartment.connection.execute(pg_migrations_data) 149 | end 150 | 151 | # Dump postgres default schema 152 | # 153 | # @return {String} raw SQL contaning only postgres schema dump 154 | # 155 | def pg_dump_schema 156 | 157 | # Skip excluded tables? :/ 158 | # excluded_tables = 159 | # collect_table_names(Apartment.excluded_models) 160 | # .map! {|t| "-T #{t}"} 161 | # .join(' ') 162 | 163 | # `pg_dump -s -x -O -n #{default_tenant} #{excluded_tables} #{dbname}` 164 | 165 | with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname}` } 166 | end 167 | 168 | # Dump data from schema_migrations table 169 | # 170 | # @return {String} raw SQL contaning inserts with data from schema_migrations 171 | # 172 | def pg_dump_schema_migrations_data 173 | with_pg_env { `pg_dump -a --inserts -t #{default_tenant}.schema_migrations -t #{default_tenant}.ar_internal_metadata #{dbname}` } 174 | end 175 | 176 | # Temporary set Postgresql related environment variables if there are in @config 177 | # 178 | def with_pg_env(&block) 179 | pghost, pgport, pguser, pgpassword = ENV['PGHOST'], ENV['PGPORT'], ENV['PGUSER'], ENV['PGPASSWORD'] 180 | 181 | ENV['PGHOST'] = @config[:host] if @config[:host] 182 | ENV['PGPORT'] = @config[:port].to_s if @config[:port] 183 | ENV['PGUSER'] = @config[:username].to_s if @config[:username] 184 | ENV['PGPASSWORD'] = @config[:password].to_s if @config[:password] 185 | 186 | block.call 187 | ensure 188 | ENV['PGHOST'], ENV['PGPORT'], ENV['PGUSER'], ENV['PGPASSWORD'] = pghost, pgport, pguser, pgpassword 189 | end 190 | 191 | # Remove "SET search_path ..." line from SQL dump and prepend search_path set to current tenant 192 | # 193 | # @return {String} patched raw SQL dump 194 | # 195 | def patch_search_path(sql) 196 | search_path = "SET search_path = \"#{current}\", #{default_tenant};" 197 | 198 | swap_schema_qualifier(sql) 199 | .split("\n") 200 | .select {|line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty?} 201 | .prepend(search_path) 202 | .join("\n") 203 | end 204 | 205 | def swap_schema_qualifier(sql) 206 | sql.gsub(/#{default_tenant}\.\w*/) do |match| 207 | if Apartment.pg_excluded_names.any? { |name| match.include? name } 208 | match 209 | else 210 | match.gsub("#{default_tenant}.", %{"#{current}".}) 211 | end 212 | end 213 | end 214 | 215 | # Checks if any of regexps matches against input 216 | # 217 | def check_input_against_regexps(input, regexps) 218 | regexps.select {|c| input.match c} 219 | end 220 | 221 | # Collect table names from AR Models 222 | # 223 | def collect_table_names(models) 224 | models.map do |m| 225 | m.constantize.table_name 226 | end 227 | end 228 | 229 | # Convenience method for current database name 230 | # 231 | def dbname 232 | Apartment.connection_config[:database] 233 | end 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /lib/apartment/adapters/sqlite3_adapter.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/adapters/abstract_adapter' 2 | 3 | module Apartment 4 | module Tenant 5 | def self.sqlite3_adapter(config) 6 | Adapters::Sqlite3Adapter.new(config) 7 | end 8 | end 9 | 10 | module Adapters 11 | class Sqlite3Adapter < AbstractAdapter 12 | def initialize(config) 13 | @default_dir = File.expand_path(File.dirname(config[:database])) 14 | 15 | super 16 | end 17 | 18 | def drop(tenant) 19 | raise TenantNotFound, 20 | "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant)) 21 | 22 | File.delete(database_file(tenant)) 23 | end 24 | 25 | def current 26 | File.basename(Apartment.connection.instance_variable_get(:@config)[:database], '.sqlite3') 27 | end 28 | 29 | protected 30 | 31 | def connect_to_new(tenant) 32 | raise TenantNotFound, 33 | "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant)) 34 | 35 | super database_file(tenant) 36 | end 37 | 38 | def create_tenant(tenant) 39 | raise TenantExists, 40 | "The tenant #{environmentify(tenant)} already exists." if File.exists?(database_file(tenant)) 41 | 42 | begin 43 | f = File.new(database_file(tenant), File::CREAT) 44 | ensure 45 | f.close 46 | end 47 | end 48 | 49 | private 50 | 51 | def database_file(tenant) 52 | "#{@default_dir}/#{environmentify(tenant)}.sqlite3" 53 | end 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/apartment/console.rb: -------------------------------------------------------------------------------- 1 | # A workaraound to get `reload!` to also call Apartment::Tenant.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::Tenant.init 11 | true 12 | end 13 | -------------------------------------------------------------------------------- /lib/apartment/deprecation.rb: -------------------------------------------------------------------------------- 1 | require 'active_support/deprecation' 2 | 3 | module Apartment 4 | module Deprecation 5 | 6 | def self.warn(message) 7 | ActiveSupport::Deprecation.warn message 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/apartment/elevators/domain.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/elevators/generic' 2 | 3 | module Apartment 4 | module Elevators 5 | # Provides a rack based tenant switching solution based on domain 6 | # Assumes that tenant name should match domain 7 | # Parses request host for second level domain, ignoring www 8 | # eg. example.com => example 9 | # www.example.bc.ca => example 10 | # a.example.bc.ca => a 11 | # 12 | # 13 | class Domain < Generic 14 | 15 | def parse_tenant_name(request) 16 | return nil if request.host.blank? 17 | 18 | request.host.match(/(www\.)?(?[^.]*)/)["sld"] 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/apartment/elevators/first_subdomain.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/elevators/subdomain' 2 | 3 | module Apartment 4 | module Elevators 5 | # Provides a rack based tenant switching solution based on the first subdomain 6 | # of a given domain name. 7 | # eg: 8 | # - example1.domain.com => example1 9 | # - example2.something.domain.com => example2 10 | class FirstSubdomain < Subdomain 11 | 12 | def parse_tenant_name(request) 13 | super.split('.')[0] unless super.nil? 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/apartment/elevators/generic.rb: -------------------------------------------------------------------------------- 1 | require 'rack/request' 2 | require 'apartment/tenant' 3 | 4 | module Apartment 5 | module Elevators 6 | # Provides a rack based tenant switching solution based on request 7 | # 8 | class Generic 9 | 10 | def initialize(app, processor = nil) 11 | @app = app 12 | @processor = processor || method(:parse_tenant_name) 13 | end 14 | 15 | def call(env) 16 | request = Rack::Request.new(env) 17 | 18 | database = @processor.call(request) 19 | 20 | if database 21 | Apartment::Tenant.switch(database) { @app.call(env) } 22 | else 23 | @app.call(env) 24 | end 25 | end 26 | 27 | def parse_tenant_name(request) 28 | raise "Override" 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/apartment/elevators/host.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/elevators/generic' 2 | 3 | module Apartment 4 | module Elevators 5 | # Provides a rack based tenant switching solution based on the host 6 | # Assumes that tenant name should match host 7 | # Strips/ignores first subdomains in ignored_first_subdomains 8 | # eg. example.com => example.com 9 | # www.example.bc.ca => www.example.bc.ca 10 | # if ignored_first_subdomains = ['www'] 11 | # www.example.bc.ca => example.bc.ca 12 | # www.a.b.c.d.com => a.b.c.d.com 13 | # 14 | class Host < Generic 15 | def self.ignored_first_subdomains 16 | @ignored_first_subdomains ||= [] 17 | end 18 | 19 | def self.ignored_first_subdomains=(arg) 20 | @ignored_first_subdomains = arg 21 | end 22 | 23 | def parse_tenant_name(request) 24 | return nil if request.host.blank? 25 | parts = request.host.split('.') 26 | self.class.ignored_first_subdomains.include?(parts[0]) ? parts.drop(1).join('.') : request.host 27 | end 28 | end 29 | end 30 | end -------------------------------------------------------------------------------- /lib/apartment/elevators/host_hash.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/elevators/generic' 2 | 3 | module Apartment 4 | module Elevators 5 | # Provides a rack based tenant switching solution based on hosts 6 | # Uses a hash to find the corresponding tenant name for the host 7 | # 8 | class HostHash < Generic 9 | def initialize(app, hash = {}, processor = nil) 10 | super app, processor 11 | @hash = hash 12 | end 13 | 14 | def parse_tenant_name(request) 15 | raise TenantNotFound, 16 | "Cannot find tenant for host #{request.host}" unless @hash.has_key?(request.host) 17 | 18 | @hash[request.host] 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/apartment/elevators/subdomain.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/elevators/generic' 2 | require 'public_suffix' 3 | 4 | module Apartment 5 | module Elevators 6 | # Provides a rack based tenant switching solution based on subdomains 7 | # Assumes that tenant name should match subdomain 8 | # 9 | class Subdomain < Generic 10 | def self.excluded_subdomains 11 | @excluded_subdomains ||= [] 12 | end 13 | 14 | def self.excluded_subdomains=(arg) 15 | @excluded_subdomains = arg 16 | end 17 | 18 | def parse_tenant_name(request) 19 | request_subdomain = subdomain(request.host) 20 | 21 | # If the domain acquired is set to be excluded, set the tenant to whatever is currently 22 | # next in line in the schema search path. 23 | tenant = if self.class.excluded_subdomains.include?(request_subdomain) 24 | nil 25 | else 26 | request_subdomain 27 | end 28 | 29 | tenant.presence 30 | end 31 | 32 | protected 33 | 34 | # *Almost* a direct ripoff of ActionDispatch::Request subdomain methods 35 | 36 | # Only care about the first subdomain for the database name 37 | def subdomain(host) 38 | subdomains(host).first 39 | end 40 | 41 | def subdomains(host) 42 | host_valid?(host) ? parse_host(host) : [] 43 | end 44 | 45 | def host_valid?(host) 46 | !ip_host?(host) && domain_valid?(host) 47 | end 48 | 49 | def ip_host?(host) 50 | !/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.match(host).nil? 51 | end 52 | 53 | def domain_valid?(host) 54 | PublicSuffix.valid?(host, ignore_private: true) 55 | end 56 | 57 | def parse_host(host) 58 | (PublicSuffix.parse(host, ignore_private: true).trd || '').split('.') 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/apartment/migrator.rb: -------------------------------------------------------------------------------- 1 | require 'apartment/tenant' 2 | 3 | module Apartment 4 | module Migrator 5 | 6 | extend self 7 | 8 | # Migrate to latest 9 | def migrate(database) 10 | Tenant.switch(database) do 11 | version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil 12 | 13 | migration_scope_block = -> (migration) { ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) } 14 | 15 | if activerecord_below_5_2? 16 | ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version, &migration_scope_block) 17 | else 18 | ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block) 19 | end 20 | end 21 | end 22 | 23 | # Migrate up/down to a specific version 24 | def run(direction, database, version) 25 | Tenant.switch(database) do 26 | if activerecord_below_5_2? 27 | ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version) 28 | else 29 | ActiveRecord::Base.connection.migration_context.run(direction, version) 30 | end 31 | end 32 | end 33 | 34 | # rollback latest migration `step` number of times 35 | def rollback(database, step = 1) 36 | Tenant.switch(database) do 37 | if activerecord_below_5_2? 38 | ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step) 39 | else 40 | ActiveRecord::Base.connection.migration_context.rollback(step) 41 | end 42 | end 43 | end 44 | 45 | private 46 | 47 | def activerecord_below_5_2? 48 | ActiveRecord.version.release < Gem::Version.new('5.2.0') 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/apartment/railtie.rb: -------------------------------------------------------------------------------- 1 | require 'rails' 2 | require 'apartment/tenant' 3 | require 'apartment/reloader' 4 | 5 | module Apartment 6 | class Railtie < Rails::Railtie 7 | 8 | # 9 | # Set up our default config options 10 | # Do this before the app initializers run so we don't override custom settings 11 | # 12 | config.before_initialize do 13 | Apartment.configure do |config| 14 | config.excluded_models = [] 15 | config.use_schemas = true 16 | config.tenant_names = [] 17 | config.seed_after_create = false 18 | config.prepend_environment = false 19 | config.append_environment = false 20 | end 21 | 22 | ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a 23 | end 24 | 25 | # Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized 26 | # Note that this doens't entirely work as expected in Development, because this is called before classes are reloaded 27 | # See the middleware/console declarations below to help with this. Hope to fix that soon. 28 | # 29 | config.to_prepare do 30 | next if ARGV.any? { |arg| arg =~ /\Aassets:(?:precompile|clean)\z/ } 31 | 32 | begin 33 | Apartment.connection_class.connection_pool.with_connection do 34 | Apartment::Tenant.init 35 | end 36 | rescue ::ActiveRecord::NoDatabaseError 37 | # Since `db:create` and other tasks invoke this block from Rails 5.2.0, 38 | # we need to swallow the error to execute `db:create` properly. 39 | end 40 | end 41 | 42 | # 43 | # Ensure rake tasks are loaded 44 | # 45 | rake_tasks do 46 | load 'tasks/apartment.rake' 47 | require 'apartment/tasks/enhancements' if Apartment.db_migrate_tenants 48 | end 49 | 50 | # 51 | # The following initializers are a workaround to the fact that I can't properly hook into the rails reloader 52 | # Note this is technically valid for any environment where cache_classes is false, for us, it's just development 53 | # 54 | if Rails.env.development? 55 | 56 | # Apartment::Reloader is middleware to initialize things properly on each request to dev 57 | initializer 'apartment.init' do |app| 58 | app.config.middleware.use Apartment::Reloader 59 | end 60 | 61 | # Overrides reload! to also call Apartment::Tenant.init as well so that the reloaded classes have the proper table_names 62 | console do 63 | require 'apartment/console' 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/apartment/reloader.rb: -------------------------------------------------------------------------------- 1 | module Apartment 2 | class Reloader 3 | 4 | # Middleware used in development to init Apartment for each request 5 | # Necessary due to code reload (annoying). When models are reloaded, they no longer have the proper table_name 6 | # That is prepended with the schema (if using postgresql schemas) 7 | # I couldn't figure out how to properly hook into the Rails reload process *after* files are reloaded 8 | # so I've used this in the meantime. 9 | # 10 | # Also see apartment/console for the re-definition of reload! that re-init's Apartment 11 | # 12 | def initialize(app) 13 | @app = app 14 | end 15 | 16 | def call(env) 17 | Tenant.init 18 | @app.call(env) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/apartment/tasks/enhancements.rb: -------------------------------------------------------------------------------- 1 | # Require this file to append Apartment rake tasks to ActiveRecord db rake tasks 2 | # Enabled by default in the initializer 3 | 4 | module Apartment 5 | class RakeTaskEnhancer 6 | 7 | module TASKS 8 | ENHANCE_BEFORE = %w(db:drop) 9 | ENHANCE_AFTER = %w(db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo db:seed) 10 | freeze 11 | end 12 | 13 | # This is a bit convoluted, but helps solve problems when using Apartment within an engine 14 | # See spec/integration/use_within_an_engine.rb 15 | 16 | class << self 17 | def enhance! 18 | return unless should_enhance? 19 | 20 | # insert task before 21 | TASKS::ENHANCE_BEFORE.each do |name| 22 | task = Rake::Task[name] 23 | enhance_before_task(task) 24 | end 25 | 26 | # insert task after 27 | TASKS::ENHANCE_AFTER.each do |name| 28 | task = Rake::Task[name] 29 | enhance_after_task(task) 30 | end 31 | 32 | end 33 | 34 | def should_enhance? 35 | Apartment.db_migrate_tenants 36 | end 37 | 38 | def enhance_before_task(task) 39 | task.enhance([inserted_task_name(task)]) 40 | end 41 | 42 | def enhance_after_task(task) 43 | task.enhance do 44 | Rake::Task[inserted_task_name(task)].invoke 45 | end 46 | end 47 | 48 | def inserted_task_name(task) 49 | task.name.sub(/db:/, 'apartment:') 50 | end 51 | 52 | end 53 | 54 | end 55 | end 56 | 57 | Apartment::RakeTaskEnhancer.enhance! 58 | -------------------------------------------------------------------------------- /lib/apartment/tenant.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module Apartment 4 | # The main entry point to Apartment functions 5 | # 6 | module Tenant 7 | 8 | extend self 9 | extend Forwardable 10 | 11 | def_delegators :adapter, :create, :drop, :switch, :switch!, :current, :each, :reset, :set_callback, :seed, :current_tenant, :default_tenant, :environmentify 12 | 13 | attr_writer :config 14 | 15 | # Initialize Apartment config options such as excluded_models 16 | # 17 | def init 18 | adapter.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 | Thread.current[:apartment_adapter] ||= begin 27 | adapter_method = "#{config[:adapter]}_adapter" 28 | 29 | if defined?(JRUBY_VERSION) 30 | if config[:adapter] =~ /mysql/ 31 | adapter_method = 'jdbc_mysql_adapter' 32 | elsif config[:adapter] =~ /postgresql/ 33 | adapter_method = 'jdbc_postgresql_adapter' 34 | end 35 | end 36 | 37 | begin 38 | require "apartment/adapters/#{adapter_method}" 39 | rescue LoadError 40 | raise "The adapter `#{adapter_method}` is not yet supported" 41 | end 42 | 43 | unless respond_to?(adapter_method) 44 | raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter" 45 | end 46 | 47 | send(adapter_method, config) 48 | end 49 | end 50 | 51 | # Reset config and adapter so they are regenerated 52 | # 53 | def reload!(config = nil) 54 | Thread.current[:apartment_adapter] = nil 55 | @config = config 56 | end 57 | 58 | private 59 | 60 | # Fetch the rails database configuration 61 | # 62 | def config 63 | @config ||= Apartment.connection_config 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/apartment/version.rb: -------------------------------------------------------------------------------- 1 | module Apartment 2 | VERSION = "2.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/apartment/install/USAGE: -------------------------------------------------------------------------------- 1 | Description: 2 | Creates an initializer for apartment. 3 | 4 | Example: 5 | `rails generate apartment:install` 6 | -------------------------------------------------------------------------------- /lib/generators/apartment/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Apartment 2 | class InstallGenerator < Rails::Generators::Base 3 | source_root File.expand_path('../templates', __FILE__) 4 | 5 | def copy_files 6 | template "apartment.rb", File.join("config", "initializers", "apartment.rb") 7 | end 8 | 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/apartment/install/templates/apartment.rb: -------------------------------------------------------------------------------- 1 | # You can have Apartment route to the appropriate Tenant by adding some Rack middleware. 2 | # Apartment can support many different "Elevators" that can take care of this routing to your data. 3 | # Require whichever Elevator you're using below or none if you have a custom one. 4 | # 5 | # require 'apartment/elevators/generic' 6 | # require 'apartment/elevators/domain' 7 | require 'apartment/elevators/subdomain' 8 | # require 'apartment/elevators/first_subdomain' 9 | # require 'apartment/elevators/host' 10 | 11 | # 12 | # Apartment Configuration 13 | # 14 | Apartment.configure do |config| 15 | 16 | # Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace. 17 | # A typical example would be a Customer or Tenant model that stores each Tenant's information. 18 | # 19 | # config.excluded_models = %w{ Tenant } 20 | 21 | # In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment. 22 | # You can make this dynamic by providing a Proc object to be called on migrations. 23 | # This object should yield either: 24 | # - an array of strings representing each Tenant name. 25 | # - a hash which keys are tenant names, and values custom db config (must contain all key/values required in database.yml) 26 | # 27 | # config.tenant_names = lambda{ Customer.pluck(:tenant_name) } 28 | # config.tenant_names = ['tenant1', 'tenant2'] 29 | # config.tenant_names = { 30 | # 'tenant1' => { 31 | # adapter: 'postgresql', 32 | # host: 'some_server', 33 | # port: 5555, 34 | # database: 'postgres' # this is not the name of the tenant's db 35 | # # but the name of the database to connect to before creating the tenant's db 36 | # # mandatory in postgresql 37 | # }, 38 | # 'tenant2' => { 39 | # adapter: 'postgresql', 40 | # database: 'postgres' # this is not the name of the tenant's db 41 | # # but the name of the database to connect to before creating the tenant's db 42 | # # mandatory in postgresql 43 | # } 44 | # } 45 | # config.tenant_names = lambda do 46 | # Tenant.all.each_with_object({}) do |tenant, hash| 47 | # hash[tenant.name] = tenant.db_configuration 48 | # end 49 | # end 50 | # 51 | config.tenant_names = lambda { ToDo_Tenant_Or_User_Model.pluck :database } 52 | 53 | # PostgreSQL: 54 | # Specifies whether to use PostgreSQL schemas or create a new database per Tenant. 55 | # 56 | # MySQL: 57 | # Specifies whether to switch databases by using `use` statement or re-establish connection. 58 | # 59 | # The default behaviour is true. 60 | # 61 | # config.use_schemas = true 62 | 63 | # 64 | # ==> PostgreSQL only options 65 | 66 | # Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas. 67 | # Use this when you are using some extra features in PostgreSQL that can't be represented in 68 | # schema.rb, like materialized views etc. (only applies with use_schemas set to true). 69 | # (Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump) 70 | # 71 | # config.use_sql = false 72 | 73 | # There are cases where you might want some schemas to always be in your search_path 74 | # e.g when using a PostgreSQL extension like hstore. 75 | # Any schemas added here will be available along with your selected Tenant. 76 | # 77 | # config.persistent_schemas = %w{ hstore } 78 | 79 | # <== PostgreSQL only options 80 | # 81 | 82 | # By default, and only when not using PostgreSQL schemas, Apartment will prepend the environment 83 | # to the tenant name to ensure there is no conflict between your environments. 84 | # This is mainly for the benefit of your development and test environments. 85 | # Uncomment the line below if you want to disable this behaviour in production. 86 | # 87 | # config.prepend_environment = !Rails.env.production? 88 | 89 | # When using PostgreSQL schemas, the database dump will be namespaced, and 90 | # apartment will substitute the default namespace (usually public) with the 91 | # name of the new tenant when creating a new tenant. Some items must maintain 92 | # a reference to the default namespace (ie public) - for instance, a default 93 | # uuid generation. Uncomment the line below to create a list of namespaced 94 | # items in the schema dump that should *not* have their namespace replaced by 95 | # the new tenant 96 | # 97 | # config.pg_excluded_names = ["uuid_generate_v4"] 98 | end 99 | 100 | # Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that 101 | # you want to switch to. 102 | # Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request| 103 | # request.host.split('.').first 104 | # } 105 | 106 | # Rails.application.config.middleware.use Apartment::Elevators::Domain 107 | Rails.application.config.middleware.use Apartment::Elevators::Subdomain 108 | # Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain 109 | # Rails.application.config.middleware.use Apartment::Elevators::Host 110 | -------------------------------------------------------------------------------- /lib/tasks/apartment.rake: -------------------------------------------------------------------------------- 1 | require 'apartment/migrator' 2 | require 'parallel' 3 | 4 | apartment_namespace = namespace :apartment do 5 | 6 | desc "Create all tenants" 7 | task :create do 8 | tenants.each do |tenant| 9 | begin 10 | puts("Creating #{tenant} tenant") 11 | Apartment::Tenant.create(tenant) 12 | rescue Apartment::TenantExists => e 13 | puts e.message 14 | end 15 | end 16 | end 17 | 18 | desc "Drop all tenants" 19 | task :drop do 20 | tenants.each do |tenant| 21 | begin 22 | puts("Dropping #{tenant} tenant") 23 | Apartment::Tenant.drop(tenant) 24 | rescue Apartment::TenantNotFound => e 25 | puts e.message 26 | end 27 | end 28 | end 29 | 30 | desc "Migrate all tenants" 31 | task :migrate do 32 | warn_if_tenants_empty 33 | each_tenant do |tenant| 34 | begin 35 | puts("Migrating #{tenant} tenant") 36 | Apartment::Migrator.migrate tenant 37 | rescue Apartment::TenantNotFound => e 38 | puts e.message 39 | end 40 | end 41 | end 42 | 43 | desc "Seed all tenants" 44 | task :seed do 45 | warn_if_tenants_empty 46 | 47 | each_tenant do |tenant| 48 | begin 49 | puts("Seeding #{tenant} tenant") 50 | Apartment::Tenant.switch(tenant) do 51 | Apartment::Tenant.seed 52 | end 53 | rescue Apartment::TenantNotFound => e 54 | puts e.message 55 | end 56 | end 57 | end 58 | 59 | desc "Rolls the migration back to the previous version (specify steps w/ STEP=n) across all tenants." 60 | task :rollback do 61 | warn_if_tenants_empty 62 | 63 | step = ENV['STEP'] ? ENV['STEP'].to_i : 1 64 | 65 | each_tenant do |tenant| 66 | begin 67 | puts("Rolling back #{tenant} tenant") 68 | Apartment::Migrator.rollback tenant, step 69 | rescue Apartment::TenantNotFound => e 70 | puts e.message 71 | end 72 | end 73 | end 74 | 75 | namespace :migrate do 76 | desc 'Runs the "up" for a given migration VERSION across all tenants.' 77 | task :up do 78 | warn_if_tenants_empty 79 | 80 | version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil 81 | raise 'VERSION is required' unless version 82 | 83 | each_tenant do |tenant| 84 | begin 85 | puts("Migrating #{tenant} tenant up") 86 | Apartment::Migrator.run :up, tenant, version 87 | rescue Apartment::TenantNotFound => e 88 | puts e.message 89 | end 90 | end 91 | end 92 | 93 | desc 'Runs the "down" for a given migration VERSION across all tenants.' 94 | task :down do 95 | warn_if_tenants_empty 96 | 97 | version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil 98 | raise 'VERSION is required' unless version 99 | 100 | each_tenant do |tenant| 101 | begin 102 | puts("Migrating #{tenant} tenant down") 103 | Apartment::Migrator.run :down, tenant, version 104 | rescue Apartment::TenantNotFound => e 105 | puts e.message 106 | end 107 | end 108 | end 109 | 110 | desc 'Rolls back the tenant one migration and re migrate up (options: STEP=x, VERSION=x).' 111 | task :redo do 112 | if ENV['VERSION'] 113 | apartment_namespace['migrate:down'].invoke 114 | apartment_namespace['migrate:up'].invoke 115 | else 116 | apartment_namespace['rollback'].invoke 117 | apartment_namespace['migrate'].invoke 118 | end 119 | end 120 | end 121 | 122 | def each_tenant(&block) 123 | Parallel.each(tenants, in_threads: Apartment.parallel_migration_threads) do |tenant| 124 | block.call(tenant) 125 | end 126 | end 127 | 128 | def tenants 129 | ENV['DB'] ? ENV['DB'].split(',').map { |s| s.strip } : Apartment.tenant_names || [] 130 | end 131 | 132 | def warn_if_tenants_empty 133 | if tenants.empty? && ENV['IGNORE_EMPTY_TENANTS'] != "true" 134 | puts <<-WARNING 135 | [WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things: 136 | 137 | 1. You may not have created any, in which case you can ignore this message 138 | 2. You've run `apartment:migrate` directly without loading the Rails environment 139 | * `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate` 140 | 141 | Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this. 142 | WARNING 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /spec/adapters/jdbc_mysql_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | if defined?(JRUBY_VERSION) 2 | 3 | require 'spec_helper' 4 | require 'apartment/adapters/jdbc_mysql_adapter' 5 | 6 | describe Apartment::Adapters::JDBCMysqlAdapter, database: :mysql do 7 | 8 | subject { Apartment::Tenant.jdbc_mysql_adapter config.symbolize_keys } 9 | 10 | def tenant_names 11 | ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect { |row| row['schema_name'] } 12 | end 13 | 14 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.current_database } } 15 | 16 | it_should_behave_like "a generic apartment adapter" 17 | it_should_behave_like "a connection based apartment adapter" 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/adapters/jdbc_postgresql_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | if defined?(JRUBY_VERSION) 2 | 3 | require 'spec_helper' 4 | require 'apartment/adapters/jdbc_postgresql_adapter' 5 | 6 | describe Apartment::Adapters::JDBCPostgresqlAdapter, database: :postgresql do 7 | 8 | subject { Apartment::Tenant.jdbc_postgresql_adapter config.symbolize_keys } 9 | 10 | context "using schemas" do 11 | 12 | before { Apartment.use_schemas = true } 13 | 14 | # Not sure why, but somehow using let(:tenant_names) memoizes for the whole example group, not just each test 15 | def tenant_names 16 | ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect { |row| row['nspname'] } 17 | end 18 | 19 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.schema_search_path.gsub('"', '') } } 20 | 21 | it_should_behave_like "a generic apartment adapter" 22 | it_should_behave_like "a schema based apartment adapter" 23 | end 24 | 25 | context "using databases" do 26 | 27 | before { Apartment.use_schemas = false } 28 | 29 | # Not sure why, but somehow using let(:tenant_names) memoizes for the whole example group, not just each test 30 | def tenant_names 31 | connection.execute("select datname from pg_database;").collect { |row| row['datname'] } 32 | end 33 | 34 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.current_database } } 35 | 36 | it_should_behave_like "a generic apartment adapter" 37 | it_should_behave_like "a connection based apartment adapter" 38 | 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/adapters/mysql2_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/adapters/mysql2_adapter' 3 | 4 | describe Apartment::Adapters::Mysql2Adapter, database: :mysql do 5 | unless defined?(JRUBY_VERSION) 6 | 7 | subject(:adapter){ Apartment::Tenant.mysql2_adapter config } 8 | 9 | def tenant_names 10 | ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect { |row| row[0] } 11 | end 12 | 13 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.current_database } } 14 | 15 | context "using - the equivalent of - schemas" do 16 | before { Apartment.use_schemas = true } 17 | 18 | it_should_behave_like "a generic apartment adapter" 19 | 20 | describe "#default_tenant" do 21 | it "is set to the original db from config" do 22 | expect(subject.default_tenant).to eq(config[:database]) 23 | end 24 | end 25 | 26 | describe "#init" do 27 | include Apartment::Spec::AdapterRequirements 28 | 29 | before do 30 | Apartment.configure do |config| 31 | config.excluded_models = ["Company"] 32 | end 33 | end 34 | 35 | after do 36 | # Apartment::Tenant.init creates per model connection. 37 | # Remove the connection after testing not to unintentionally keep the connection across tests. 38 | Apartment.excluded_models.each do |excluded_model| 39 | excluded_model.constantize.remove_connection 40 | end 41 | end 42 | 43 | it "should process model exclusions" do 44 | Apartment::Tenant.init 45 | 46 | expect(Company.table_name).to eq("#{default_tenant}.companies") 47 | end 48 | end 49 | end 50 | 51 | context "using connections" do 52 | before { Apartment.use_schemas = false } 53 | 54 | it_should_behave_like "a generic apartment adapter" 55 | it_should_behave_like "a generic apartment adapter able to handle custom configuration" 56 | it_should_behave_like "a connection based apartment adapter" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/adapters/postgresql_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/adapters/postgresql_adapter' 3 | 4 | describe Apartment::Adapters::PostgresqlAdapter, database: :postgresql do 5 | unless defined?(JRUBY_VERSION) 6 | 7 | subject{ Apartment::Tenant.postgresql_adapter config } 8 | 9 | context "using schemas with schema.rb" do 10 | 11 | before{ Apartment.use_schemas = true } 12 | 13 | # Not sure why, but somehow using let(:tenant_names) memoizes for the whole example group, not just each test 14 | def tenant_names 15 | ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect { |row| row['nspname'] } 16 | end 17 | 18 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.schema_search_path.gsub('"', '') } } 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 schemas with SQL dump" do 25 | 26 | before{ Apartment.use_schemas = true; Apartment.use_sql = true } 27 | 28 | # Not sure why, but somehow using let(:tenant_names) memoizes for the whole example group, not just each test 29 | def tenant_names 30 | ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect { |row| row['nspname'] } 31 | end 32 | 33 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.schema_search_path.gsub('"', '') } } 34 | 35 | it_should_behave_like "a generic apartment adapter" 36 | it_should_behave_like "a schema based apartment adapter" 37 | 38 | it 'allows for dashes in the schema name' do 39 | expect { Apartment::Tenant.create('has-dashes') }.to_not raise_error 40 | end 41 | 42 | after { Apartment::Tenant.drop('has-dashes') if Apartment.connection.schema_exists? 'has-dashes' } 43 | end 44 | 45 | context "using connections" do 46 | 47 | before{ Apartment.use_schemas = false } 48 | 49 | # Not sure why, but somehow using let(:tenant_names) memoizes for the whole example group, not just each test 50 | def tenant_names 51 | connection.execute("select datname from pg_database;").collect { |row| row['datname'] } 52 | end 53 | 54 | let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.current_database } } 55 | 56 | it_should_behave_like "a generic apartment adapter" 57 | it_should_behave_like "a generic apartment adapter able to handle custom configuration" 58 | it_should_behave_like "a connection based apartment adapter" 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/adapters/sqlite3_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/adapters/sqlite3_adapter' 3 | 4 | describe Apartment::Adapters::Sqlite3Adapter, database: :sqlite do 5 | unless defined?(JRUBY_VERSION) 6 | 7 | subject{ Apartment::Tenant.sqlite3_adapter config } 8 | 9 | context "using connections" do 10 | def tenant_names 11 | db_dir = File.expand_path("../../dummy/db", __FILE__) 12 | Dir.glob("#{db_dir}/*.sqlite3").map { |file| File.basename(file, '.sqlite3') } 13 | end 14 | 15 | let(:default_tenant) do 16 | subject.switch { File.basename(Apartment::Test.config['connections']['sqlite']['database'], '.sqlite3') } 17 | end 18 | 19 | it_should_behave_like "a generic apartment adapter" 20 | it_should_behave_like "a connection based apartment adapter" 21 | 22 | after(:all) do 23 | File.delete(Apartment::Test.config['connections']['sqlite']['database']) 24 | end 25 | end 26 | 27 | context "with prepend and append" do 28 | let(:default_dir) { File.expand_path(File.dirname(config[:database])) } 29 | describe "#prepend" do 30 | let (:db_name) { "db_with_prefix" } 31 | before do 32 | Apartment.configure do |config| 33 | config.prepend_environment = true 34 | config.append_environment = false 35 | end 36 | end 37 | 38 | after { subject.drop db_name rescue nil } 39 | 40 | it "should create a new database" do 41 | subject.create db_name 42 | 43 | expect(File.exists?("#{default_dir}/#{Rails.env}_#{db_name}.sqlite3")).to eq true 44 | end 45 | end 46 | 47 | describe "#neither" do 48 | let (:db_name) { "db_without_prefix_suffix" } 49 | before do 50 | Apartment.configure { |config| config.prepend_environment = config.append_environment = false } 51 | end 52 | 53 | after { subject.drop db_name rescue nil } 54 | 55 | it "should create a new database" do 56 | subject.create db_name 57 | 58 | expect(File.exists?("#{default_dir}/#{db_name}.sqlite3")).to eq true 59 | end 60 | end 61 | 62 | describe "#append" do 63 | let (:db_name) { "db_with_suffix" } 64 | before do 65 | Apartment.configure do |config| 66 | config.prepend_environment = false 67 | config.append_environment = true 68 | end 69 | end 70 | 71 | after { subject.drop db_name rescue nil } 72 | 73 | it "should create a new database" do 74 | subject.create db_name 75 | 76 | expect(File.exists?("#{default_dir}/#{db_name}_#{Rails.env}.sqlite3")).to eq true 77 | end 78 | end 79 | 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/apartment_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Apartment do 4 | it "should be valid" do 5 | expect(Apartment).to be_a(Module) 6 | end 7 | 8 | it "should be a valid app" do 9 | expect(::Rails.application).to be_a(Dummy::Application) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/config/database.yml.sample: -------------------------------------------------------------------------------- 1 | <% if defined?(JRUBY_VERSION) %> 2 | connections: 3 | postgresql: 4 | adapter: postgresql 5 | database: apartment_postgresql_test 6 | username: postgres 7 | min_messages: WARNING 8 | driver: org.postgresql.Driver 9 | url: jdbc:postgresql://localhost:5432/apartment_postgresql_test 10 | timeout: 5000 11 | pool: 5 12 | host: localhost 13 | port: 5432 14 | 15 | mysql: 16 | adapter: mysql 17 | database: apartment_mysql_test 18 | username: root 19 | min_messages: WARNING 20 | driver: com.mysql.jdbc.Driver 21 | url: jdbc:mysql://localhost:3306/apartment_mysql_test 22 | timeout: 5000 23 | pool: 5 24 | host: 127.0.0.1 25 | port: 3306 26 | <% else %> 27 | connections: 28 | postgresql: 29 | adapter: postgresql 30 | database: apartment_postgresql_test 31 | min_messages: WARNING 32 | username: postgres 33 | schema_search_path: public 34 | password: 35 | host: localhost 36 | port: 5432 37 | 38 | mysql: 39 | adapter: mysql2 40 | database: apartment_mysql_test 41 | username: root 42 | password: 43 | host: 127.0.0.1 44 | port: 3306 45 | 46 | sqlite: 47 | adapter: sqlite3 48 | database: <%= File.expand_path('../spec/dummy/db', __FILE__) %>/default.sqlite3 49 | <% end %> 50 | -------------------------------------------------------------------------------- /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/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | # Dummy models 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | # Dummy models 3 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/application/index.html.erb: -------------------------------------------------------------------------------- 1 |

Index!!

-------------------------------------------------------------------------------- /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/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/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 | require 'apartment/elevators/subdomain' 18 | require 'apartment/elevators/domain' 19 | 20 | config.middleware.use Apartment::Elevators::Subdomain 21 | 22 | # Custom directories with classes and modules you want to be autoloadable. 23 | config.autoload_paths += %W(#{config.root}/lib) 24 | 25 | # Only load the plugins named here, in the order given (default is alphabetical). 26 | # :all can be used as a placeholder for all plugins not explicitly named. 27 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 28 | 29 | # Activate observers that should always be running. 30 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 31 | 32 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 33 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 34 | # config.time_zone = 'Central Time (US & Canada)' 35 | 36 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 37 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 38 | # config.i18n.default_locale = :de 39 | 40 | # JavaScript files you want as :defaults (application.js is always included). 41 | # config.action_view.javascript_expansions[:defaults] = %w(jquery rails) 42 | 43 | # Configure the default encoding used in templates for Ruby 1.9. 44 | config.encoding = "utf-8" 45 | 46 | # Configure sensitive parameters which will be filtered from the log file. 47 | config.filter_parameters += [:password] 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 4 | 5 | if File.exist?(gemfile) 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | end 10 | 11 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /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 | <% if defined?(JRUBY_VERSION) %> 5 | test: 6 | adapter: postgresql 7 | database: apartment_postgresql_test 8 | username: postgres 9 | min_messages: WARNING 10 | driver: org.postgresql.Driver 11 | url: jdbc:postgresql://localhost:5432/apartment_postgresql_test 12 | timeout: 5000 13 | pool: 5 14 | 15 | development: 16 | adapter: postgresql 17 | database: apartment_postgresql_development 18 | username: postgres 19 | min_messages: WARNING 20 | driver: org.postgresql.Driver 21 | url: jdbc:postgresql://localhost:5432/apartment_postgresql_development 22 | timeout: 5000 23 | pool: 5 24 | <% else %> 25 | test: 26 | adapter: postgresql 27 | database: apartment_postgresql_test 28 | username: postgres 29 | min_messages: WARNING 30 | pool: 5 31 | timeout: 5000 32 | host: localhost 33 | port: 5432 34 | 35 | development: 36 | adapter: postgresql 37 | database: apartment_postgresql_development 38 | username: postgres 39 | min_messages: WARNING 40 | pool: 5 41 | timeout: 5000 42 | host: localhost 43 | port: 5432 44 | <% end %> 45 | -------------------------------------------------------------------------------- /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/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 | config.eager_load = false 10 | 11 | # Log error messages when you accidentally call methods on nil. 12 | config.whiny_nils = true 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_view.debug_rjs = true 17 | config.action_controller.perform_caching = false 18 | 19 | # Don't care if the mailer can't send 20 | config.action_mailer.raise_delivery_errors = false 21 | 22 | # Print deprecation notices to the Rails logger 23 | config.active_support.deprecation = :log 24 | 25 | # Only use best-standards-support built into browsers 26 | config.action_dispatch.best_standards_support = :builtin 27 | end 28 | 29 | -------------------------------------------------------------------------------- /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 | config.eager_load = true 9 | 10 | # Full error reports are disabled and caching is turned on 11 | config.consider_all_requests_local = false 12 | config.action_controller.perform_caching = true 13 | 14 | # Specifies the header that your server uses for sending files 15 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 16 | 17 | # For nginx: 18 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 19 | 20 | # If you have no front-end server that supports something like X-Sendfile, 21 | # just comment this out and Rails will serve the files 22 | 23 | # See everything in the log (default is :info) 24 | # config.log_level = :debug 25 | 26 | # Use a different logger for distributed setups 27 | # config.logger = SyslogLogger.new 28 | 29 | # Use a different cache store in production 30 | # config.cache_store = :mem_cache_store 31 | 32 | # Disable Rails's static asset server 33 | # In production, Apache or nginx will already do this 34 | config.serve_static_assets = false 35 | 36 | # Enable serving of images, stylesheets, and javascripts from an asset server 37 | # config.action_controller.asset_host = "http://assets.example.com" 38 | 39 | # Disable delivery errors, bad email addresses will be ignored 40 | # config.action_mailer.raise_delivery_errors = false 41 | 42 | # Enable threaded mode 43 | # config.threadsafe! 44 | 45 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 46 | # the I18n.default_locale when a translation can not be found) 47 | config.i18n.fallbacks = true 48 | 49 | # Send deprecation notices to registered listeners 50 | config.active_support.deprecation = :notify 51 | end 52 | -------------------------------------------------------------------------------- /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 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Raise exceptions instead of rendering exception templates 17 | config.action_dispatch.show_exceptions = false 18 | 19 | # Disable request forgery protection in test environment 20 | config.action_controller.allow_forgery_protection = false 21 | 22 | # Tell Action Mailer not to deliver emails to the real world. 23 | # The :test delivery method accumulates sent emails in the 24 | # ActionMailer::Base.deliveries array. 25 | config.action_mailer.delivery_method = :test 26 | 27 | # Use SQL instead of Active Record's schema dumper when creating the test database. 28 | # This is necessary if your schema can't be completely dumped by the schema dumper, 29 | # like if you have constraints or database-specific column types 30 | # config.active_record.schema_format = :sql 31 | 32 | # Print deprecation notices to the stderr 33 | config.active_support.deprecation = :stderr 34 | end 35 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/apartment.rb: -------------------------------------------------------------------------------- 1 | Apartment.configure do |config| 2 | config.excluded_models = ["Company"] 3 | config.tenant_names = lambda{ Company.pluck(:database) } 4 | end 5 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | root :to => 'application#index' 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20110613152810_create_dummy_models.rb: -------------------------------------------------------------------------------- 1 | migration_class = (ActiveRecord::VERSION::MAJOR >= 5) ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration 2 | class CreateDummyModels < migration_class 3 | def self.up 4 | create_table :companies do |t| 5 | t.boolean :dummy 6 | t.string :database 7 | end 8 | 9 | create_table :users do |t| 10 | t.string :name 11 | t.datetime :birthdate 12 | t.string :sex 13 | end 14 | 15 | create_table :delayed_jobs do |t| 16 | t.integer :priority, :default => 0 17 | t.integer :attempts, :default => 0 18 | t.text :handler 19 | t.text :last_error 20 | t.datetime :run_at 21 | t.datetime :locked_at 22 | t.datetime :failed_at 23 | t.string :locked_by 24 | t.datetime :created_at 25 | t.datetime :updated_at 26 | t.string :queue 27 | end 28 | 29 | add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" 30 | 31 | end 32 | 33 | def self.down 34 | drop_table :companies 35 | drop_table :users 36 | drop_table :delayed_jobs 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20111202022214_create_table_books.rb: -------------------------------------------------------------------------------- 1 | migration_class = (ActiveRecord::VERSION::MAJOR >= 5) ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration 2 | class CreateTableBooks < migration_class 3 | def up 4 | create_table :books do |t| 5 | t.string :name 6 | t.integer :pages 7 | t.datetime :published 8 | end 9 | end 10 | 11 | def down 12 | drop_table :books 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20180415260934_create_public_tokens.rb: -------------------------------------------------------------------------------- 1 | migration_class = (ActiveRecord::VERSION::MAJOR >= 5) ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration 2 | class CreatePublicTokens < migration_class 3 | def up 4 | create_table :public_tokens do |t| 5 | t.string :token 6 | t.integer :user_id, foreign_key: true 7 | end 8 | end 9 | 10 | def down 11 | drop_table :public_tokens 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 2018_04_15_260934) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "books", force: :cascade do |t| 19 | t.string "name" 20 | t.integer "pages" 21 | t.datetime "published" 22 | end 23 | 24 | create_table "companies", force: :cascade do |t| 25 | t.boolean "dummy" 26 | t.string "database" 27 | end 28 | 29 | create_table "delayed_jobs", force: :cascade do |t| 30 | t.integer "priority", default: 0 31 | t.integer "attempts", default: 0 32 | t.text "handler" 33 | t.text "last_error" 34 | t.datetime "run_at" 35 | t.datetime "locked_at" 36 | t.datetime "failed_at" 37 | t.string "locked_by" 38 | t.datetime "created_at" 39 | t.datetime "updated_at" 40 | t.string "queue" 41 | t.index ["priority", "run_at"], name: "delayed_jobs_priority" 42 | end 43 | 44 | create_table "public_tokens", id: :serial, force: :cascade do |t| 45 | t.string "token" 46 | t.integer "user_id" 47 | end 48 | 49 | create_table "users", force: :cascade do |t| 50 | t.string "name" 51 | t.datetime "birthdate" 52 | t.string "sex" 53 | end 54 | 55 | end 56 | -------------------------------------------------------------------------------- /spec/dummy/db/seeds.rb: -------------------------------------------------------------------------------- 1 | def create_users 2 | 3.times {|x| User.where(name: "Some User #{x}").first_or_create! } 3 | end 4 | 5 | create_users -------------------------------------------------------------------------------- /spec/dummy/db/seeds/import.rb: -------------------------------------------------------------------------------- 1 | def create_users 2 | 6.times {|x| User.where(name: "Different User #{x}").first_or_create! } 3 | end 4 | 5 | create_users 6 | -------------------------------------------------------------------------------- /spec/dummy/db/test.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influitive/apartment/f266f73e58835f94e4ec7c16f28443fe5eada1ac/spec/dummy/db/test.sqlite3 -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influitive/apartment/f266f73e58835f94e4ec7c16f28443fe5eada1ac/spec/dummy/public/favicon.ico -------------------------------------------------------------------------------- /spec/dummy/public/stylesheets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/influitive/apartment/f266f73e58835f94e4ec7c16f28443fe5eada1ac/spec/dummy/public/stylesheets/.gitkeep -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /spec/dummy_engine/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle/ 2 | log/*.log 3 | pkg/ 4 | test/dummy/db/*.sqlite3 5 | test/dummy/db/*.sqlite3-journal 6 | test/dummy/log/*.log 7 | test/dummy/tmp/ 8 | test/dummy/.sass-cache 9 | -------------------------------------------------------------------------------- /spec/dummy_engine/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Declare your gem's dependencies in dummy_engine.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use debugger 14 | # gem 'debugger' 15 | gem 'apartment', path: '../../' 16 | -------------------------------------------------------------------------------- /spec/dummy_engine/Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'DummyEngine' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | 22 | Bundler::GemHelper.install_tasks 23 | 24 | require 'rake/testtask' 25 | 26 | Rake::TestTask.new(:test) do |t| 27 | t.libs << 'lib' 28 | t.libs << 'test' 29 | t.pattern = 'test/**/*_test.rb' 30 | t.verbose = false 31 | end 32 | 33 | 34 | task default: :test 35 | -------------------------------------------------------------------------------- /spec/dummy_engine/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/dummy_engine/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /spec/dummy_engine/config/initializers/apartment.rb: -------------------------------------------------------------------------------- 1 | # Require whichever elevator you're using below here... 2 | # 3 | # require 'apartment/elevators/generic' 4 | # require 'apartment/elevators/domain' 5 | require 'apartment/elevators/subdomain' 6 | 7 | # 8 | # Apartment Configuration 9 | # 10 | Apartment.configure do |config| 11 | 12 | # These models will not be multi-tenanted, 13 | # but remain in the global (public) namespace 14 | # 15 | # An example might be a Customer or Tenant model that stores each tenant information 16 | # ex: 17 | # 18 | # config.excluded_models = %w{Tenant} 19 | # 20 | config.excluded_models = %w{} 21 | 22 | # use postgres schemas? 23 | config.use_schemas = true 24 | 25 | # use raw SQL dumps for creating postgres schemas? (only appies with use_schemas set to true) 26 | #config.use_sql = true 27 | 28 | # configure persistent schemas (E.g. hstore ) 29 | # config.persistent_schemas = %w{ hstore } 30 | 31 | # add the Rails environment to database names? 32 | # config.prepend_environment = true 33 | # config.append_environment = true 34 | 35 | # supply list of database names for migrations to run on 36 | # config.tenant_names = lambda{ ToDo_Tenant_Or_User_Model.pluck :database } 37 | 38 | # Specify a connection other than ActiveRecord::Base for apartment to use (only needed if your models are using a different connection) 39 | # config.connection_class = ActiveRecord::Base 40 | end 41 | 42 | ## 43 | # Elevator Configuration 44 | 45 | # Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request| 46 | # # TODO: supply generic implementation 47 | # } 48 | 49 | # Rails.application.config.middleware.use Apartment::Elevators::Domain 50 | 51 | Rails.application.config.middleware.use Apartment::Elevators::Subdomain 52 | -------------------------------------------------------------------------------- /spec/dummy_engine/dummy_engine.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path("../lib", __FILE__) 2 | 3 | # Maintain your gem's version: 4 | require "dummy_engine/version" 5 | 6 | # Describe your gem and declare its dependencies: 7 | Gem::Specification.new do |s| 8 | s.name = "dummy_engine" 9 | s.version = DummyEngine::VERSION 10 | s.authors = ["Your name"] 11 | s.email = ["Your email"] 12 | s.homepage = "" 13 | s.summary = "Summary of DummyEngine." 14 | s.description = "Description of DummyEngine." 15 | s.license = "MIT" 16 | 17 | s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"] 18 | s.test_files = Dir["test/**/*"] 19 | 20 | s.add_dependency "rails", "~> 4.1.6" 21 | s.add_dependency "apartment" 22 | 23 | s.add_development_dependency "sqlite3" 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy_engine/lib/dummy_engine.rb: -------------------------------------------------------------------------------- 1 | require "dummy_engine/engine" 2 | 3 | module DummyEngine 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy_engine/lib/dummy_engine/engine.rb: -------------------------------------------------------------------------------- 1 | module DummyEngine 2 | class Engine < ::Rails::Engine 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy_engine/lib/dummy_engine/version.rb: -------------------------------------------------------------------------------- 1 | module DummyEngine 2 | VERSION = "0.0.1" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/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 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/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 Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(*Rails.groups) 6 | require "dummy_engine" 7 | 8 | module Dummy 9 | class Application < Rails::Application 10 | # Settings in config/environments/* take precedence over those specified here. 11 | # Application configuration should go into files in config/initializers 12 | # -- all .rb files in that directory are automatically loaded. 13 | 14 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 15 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 16 | # config.time_zone = 'Central Time (US & Canada)' 17 | 18 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 19 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 20 | # config.i18n.default_locale = :de 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.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 web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Adds additional error checking when serving assets at runtime. 31 | # Checks for improperly declared sprockets dependencies. 32 | # Raises helpful error messages. 33 | config.assets.raise_runtime_errors = true 34 | 35 | # Raises error for missing translations 36 | # config.action_view.raise_on_missing_translations = true 37 | end 38 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Ignore bad email addresses and do not raise email delivery errors. 60 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 61 | # config.action_mailer.raise_delivery_errors = false 62 | 63 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 64 | # the I18n.default_locale when a translation cannot be found). 65 | config.i18n.fallbacks = true 66 | 67 | # Send deprecation notices to registered listeners. 68 | config.active_support.deprecation = :notify 69 | 70 | # Disable automatic flushing of the log to improve performance. 71 | # config.autoflush_log = false 72 | 73 | # Use default logging formatter so that PID and timestamp are not suppressed. 74 | config.log_formatter = ::Logger::Formatter.new 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | end 79 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.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 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Precompile additional assets. 7 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 8 | # Rails.application.config.assets.precompile += %w( search.js ) 9 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/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_engine/test/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/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. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/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 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | # The priority is based upon order of creation: first created -> highest priority. 3 | # See how all your routes lay out with "rake routes". 4 | 5 | # You can have the root of your site routed with "root" 6 | # root 'welcome#index' 7 | 8 | # Example of regular route: 9 | # get 'products/:id' => 'catalog#view' 10 | 11 | # Example of named route that can be invoked with purchase_url(id: product.id) 12 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 13 | 14 | # Example resource route (maps HTTP verbs to controller actions automatically): 15 | # resources :products 16 | 17 | # Example resource route with options: 18 | # resources :products do 19 | # member do 20 | # get 'short' 21 | # post 'toggle' 22 | # end 23 | # 24 | # collection do 25 | # get 'sold' 26 | # end 27 | # end 28 | 29 | # Example resource route with sub-resources: 30 | # resources :products do 31 | # resources :comments, :sales 32 | # resource :seller 33 | # end 34 | 35 | # Example resource route with more complex sub-resources: 36 | # resources :products do 37 | # resources :comments 38 | # resources :sales do 39 | # get 'recent', on: :collection 40 | # end 41 | # end 42 | 43 | # Example resource route with concerns: 44 | # concern :toggleable do 45 | # post 'toggle' 46 | # end 47 | # resources :posts, concerns: :toggleable 48 | # resources :photos, concerns: :toggleable 49 | 50 | # Example resource route within a namespace: 51 | # namespace :admin do 52 | # # Directs /admin/products/* to Admin::ProductsController 53 | # # (app/controllers/admin/products_controller.rb) 54 | # resources :products 55 | # end 56 | end 57 | -------------------------------------------------------------------------------- /spec/dummy_engine/test/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: bb62b819b585a74e69c797f9d03d5a004d8fe82a8e7a7da6fa2f7923030713b7b087c12cc7a918e71073c38afb343f7223d22ba3f1b223b7e76dbf8d5b65fa2c 15 | 16 | test: 17 | secret_key_base: 67945d3b189c71dffef98de2bb7c14d6fb059679c115ca3cddf65c88babe130afe4d583560d0e308b017dd76ce305bef4159d876de9fd893952d9cbf269c8476 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /spec/examples/connection_adapter_examples.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples_for "a connection based apartment adapter" do 4 | include Apartment::Spec::AdapterRequirements 5 | 6 | let(:default_tenant){ subject.switch{ ActiveRecord::Base.connection.current_database } } 7 | 8 | describe "#init" do 9 | after do 10 | # Apartment::Tenant.init creates per model connection. 11 | # Remove the connection after testing not to unintentionally keep the connection across tests. 12 | Apartment.excluded_models.each do |excluded_model| 13 | excluded_model.constantize.remove_connection 14 | end 15 | end 16 | 17 | it "should process model exclusions" do 18 | Apartment.configure do |config| 19 | config.excluded_models = ["Company"] 20 | end 21 | Apartment::Tenant.init 22 | 23 | expect(Company.connection.object_id).not_to eq(ActiveRecord::Base.connection.object_id) 24 | end 25 | end 26 | 27 | describe "#drop" do 28 | it "should raise an error for unknown database" do 29 | expect { 30 | subject.drop 'unknown_database' 31 | }.to raise_error(Apartment::TenantNotFound) 32 | end 33 | end 34 | 35 | describe "#switch!" do 36 | it "should raise an error if database is invalid" do 37 | expect { 38 | subject.switch! 'unknown_database' 39 | }.to raise_error(Apartment::TenantNotFound) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/examples/generic_adapter_custom_configuration_example.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | shared_examples_for "a generic apartment adapter able to handle custom configuration" do 4 | 5 | let(:custom_tenant_name) { 'test_tenantwwww' } 6 | let(:db) { |example| example.metadata[:database]} 7 | let(:custom_tenant_names) do 8 | { 9 | custom_tenant_name => get_custom_db_conf 10 | } 11 | end 12 | 13 | before do 14 | Apartment.tenant_names = custom_tenant_names 15 | Apartment.with_multi_server_setup = true 16 | end 17 | 18 | after do 19 | Apartment.with_multi_server_setup = false 20 | end 21 | 22 | context "database key taken from specific config" do 23 | 24 | let(:expected_args) { get_custom_db_conf } 25 | 26 | describe "#create" do 27 | it "should establish_connection with the separate connection with expected args" do 28 | expect(Apartment::Adapters::AbstractAdapter::SeparateDbConnectionHandler).to receive(:establish_connection).with(expected_args).and_call_original 29 | 30 | # because we dont have another server to connect to it errors 31 | # what matters is establish_connection receives proper args 32 | expect { subject.create(custom_tenant_name) }.to raise_error(Apartment::TenantExists) 33 | end 34 | end 35 | 36 | describe "#drop" do 37 | it "should establish_connection with the separate connection with expected args" do 38 | expect(Apartment::Adapters::AbstractAdapter::SeparateDbConnectionHandler).to receive(:establish_connection).with(expected_args).and_call_original 39 | 40 | # because we dont have another server to connect to it errors 41 | # what matters is establish_connection receives proper args 42 | expect { subject.drop(custom_tenant_name) }.to raise_error(Apartment::TenantNotFound) 43 | end 44 | end 45 | end 46 | 47 | context "database key from tenant name" do 48 | 49 | let(:expected_args) { 50 | get_custom_db_conf.tap {|args| args.delete(:database) } 51 | } 52 | 53 | describe "#switch!" do 54 | 55 | it "should connect to new db" do 56 | expect(Apartment).to receive(:establish_connection) do |args| 57 | db_name = args.delete(:database) 58 | 59 | expect(args).to eq expected_args 60 | expect(db_name).to match custom_tenant_name 61 | 62 | # we only need to check args, then we short circuit 63 | # in order to avoid the mess due to the `establish_connection` override 64 | raise ActiveRecord::ActiveRecordError 65 | end 66 | 67 | expect { subject.switch!(custom_tenant_name) }.to raise_error(Apartment::TenantNotFound) 68 | end 69 | end 70 | end 71 | 72 | def specific_connection 73 | { 74 | postgresql: { 75 | adapter: 'postgresql', 76 | database: 'override_database', 77 | password: 'override_password', 78 | username: 'overridepostgres' 79 | }, 80 | mysql: { 81 | adapter: 'mysql2', 82 | database: 'override_database', 83 | username: 'root' 84 | }, 85 | sqlite: { 86 | adapter: 'sqlite3', 87 | database: 'override_database' 88 | } 89 | } 90 | end 91 | 92 | def get_custom_db_conf 93 | specific_connection[db.to_sym].with_indifferent_access 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /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 { 7 | Apartment.prepend_environment = false 8 | Apartment.append_environment = false 9 | } 10 | 11 | describe "#init" do 12 | it "should not retain a connection after railtie" do 13 | # this test should work on rails >= 4, the connection pool code is 14 | # completely different for 3.2 so we'd have to have a messy conditional.. 15 | unless Rails::VERSION::MAJOR < 4 16 | ActiveRecord::Base.connection_pool.disconnect! 17 | 18 | Apartment::Railtie.config.to_prepare_blocks.map(&:call) 19 | 20 | num_available_connections = Apartment.connection_class.connection_pool 21 | .instance_variable_get(:@available) 22 | .instance_variable_get(:@queue) 23 | .size 24 | 25 | expect(num_available_connections).to eq(1) 26 | end 27 | end 28 | end 29 | 30 | # 31 | # Creates happen already in our before_filter 32 | # 33 | describe "#create" do 34 | 35 | it "should create the new databases" do 36 | expect(tenant_names).to include(db1) 37 | expect(tenant_names).to include(db2) 38 | end 39 | 40 | it "should load schema.rb to new schema" do 41 | subject.switch(db1) do 42 | expect(connection.tables).to include('companies') 43 | end 44 | end 45 | 46 | it "should yield to block if passed and reset" do 47 | subject.drop(db2) # so we don't get errors on creation 48 | 49 | @count = 0 # set our variable so its visible in and outside of blocks 50 | 51 | subject.create(db2) do 52 | @count = User.count 53 | expect(subject.current).to eq(db2) 54 | User.create 55 | end 56 | 57 | expect(subject.current).not_to eq(db2) 58 | 59 | subject.switch(db2){ expect(User.count).to eq(@count + 1) } 60 | end 61 | 62 | it "should raise error when the schema.rb is missing unless Apartment.use_sql is set to true" do 63 | next if Apartment.use_sql 64 | 65 | subject.drop(db1) 66 | begin 67 | Dir.mktmpdir do |tmpdir| 68 | Apartment.database_schema_file = "#{tmpdir}/schema.rb" 69 | expect { 70 | subject.create(db1) 71 | }.to raise_error(Apartment::FileNotFound) 72 | end 73 | ensure 74 | Apartment.remove_instance_variable(:@database_schema_file) 75 | end 76 | end 77 | end 78 | 79 | describe "#drop" do 80 | it "should remove the db" do 81 | subject.drop db1 82 | expect(tenant_names).not_to include(db1) 83 | end 84 | end 85 | 86 | describe "#switch!" do 87 | it "should connect to new db" do 88 | subject.switch!(db1) 89 | expect(subject.current).to eq(db1) 90 | end 91 | 92 | it "should reset connection if database is nil" do 93 | subject.switch! 94 | expect(subject.current).to eq(default_tenant) 95 | end 96 | 97 | it "should raise an error if database is invalid" do 98 | expect { 99 | subject.switch! 'unknown_database' 100 | }.to raise_error(Apartment::ApartmentError) 101 | end 102 | end 103 | 104 | describe "#switch" do 105 | it "connects and resets the tenant" do 106 | subject.switch(db1) do 107 | expect(subject.current).to eq(db1) 108 | end 109 | expect(subject.current).to eq(default_tenant) 110 | end 111 | 112 | # We're often finding when using Apartment in tests, the `current` (ie the previously connect to db) 113 | # gets dropped, but switch will try to return to that db in a test. We should just reset if it doesn't exist 114 | it "should not throw exception if current is no longer accessible" do 115 | subject.switch!(db2) 116 | 117 | expect { 118 | subject.switch(db1){ subject.drop(db2) } 119 | }.to_not raise_error 120 | end 121 | end 122 | 123 | describe "#reset" do 124 | it "should reset connection" do 125 | subject.switch!(db1) 126 | subject.reset 127 | expect(subject.current).to eq(default_tenant) 128 | end 129 | end 130 | 131 | describe "#current" do 132 | it "should return the current db name" do 133 | subject.switch!(db1) 134 | expect(subject.current).to eq(db1) 135 | end 136 | end 137 | 138 | describe "#each" do 139 | it "iterates over each tenant by default" do 140 | result = [] 141 | Apartment.tenant_names = [db2, db1] 142 | 143 | subject.each do |tenant| 144 | result << tenant 145 | expect(subject.current).to eq(tenant) 146 | end 147 | 148 | expect(result).to eq([db2, db1]) 149 | end 150 | 151 | it "iterates over the given tenants" do 152 | result = [] 153 | Apartment.tenant_names = [db2] 154 | 155 | subject.each([db2]) do |tenant| 156 | result << tenant 157 | expect(subject.current).to eq(tenant) 158 | end 159 | 160 | expect(result).to eq([db2]) 161 | end 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /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_tenant } 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 | after do 19 | # Apartment::Tenant.init creates per model connection. 20 | # Remove the connection after testing not to unintentionally keep the connection across tests. 21 | Apartment.excluded_models.each do |excluded_model| 22 | excluded_model.constantize.remove_connection 23 | end 24 | end 25 | 26 | it "should process model exclusions" do 27 | Apartment::Tenant.init 28 | 29 | expect(Company.table_name).to eq("public.companies") 30 | end 31 | 32 | context "with a default_schema", :default_schema => true do 33 | 34 | it "should set the proper table_name on excluded_models" do 35 | Apartment::Tenant.init 36 | 37 | expect(Company.table_name).to eq("#{default_schema}.companies") 38 | end 39 | 40 | it 'sets the search_path correctly' do 41 | Apartment::Tenant.init 42 | 43 | expect(User.connection.schema_search_path).to match(%r|#{default_schema}|) 44 | end 45 | end 46 | 47 | context "persistent_schemas", :persistent_schemas => true do 48 | it "sets the persistent schemas in the schema_search_path" do 49 | Apartment::Tenant.init 50 | expect(connection.schema_search_path).to end_with persistent_schemas.map { |schema| %{"#{schema}"} }.join(', ') 51 | end 52 | end 53 | end 54 | 55 | # 56 | # Creates happen already in our before_filter 57 | # 58 | describe "#create" do 59 | 60 | it "should load schema.rb to new schema" do 61 | connection.schema_search_path = schema1 62 | expect(connection.tables).to include('companies') 63 | end 64 | 65 | it "should yield to block if passed and reset" do 66 | subject.drop(schema2) # so we don't get errors on creation 67 | 68 | @count = 0 # set our variable so its visible in and outside of blocks 69 | 70 | subject.create(schema2) do 71 | @count = User.count 72 | expect(connection.schema_search_path).to start_with %{"#{schema2}"} 73 | User.create 74 | end 75 | 76 | expect(connection.schema_search_path).not_to start_with %{"#{schema2}"} 77 | 78 | subject.switch(schema2){ expect(User.count).to eq(@count + 1) } 79 | end 80 | 81 | context "numeric database names" do 82 | let(:db){ 1234 } 83 | it "should allow them" do 84 | expect { 85 | subject.create(db) 86 | }.to_not raise_error 87 | expect(tenant_names).to include(db.to_s) 88 | end 89 | 90 | after{ subject.drop(db) } 91 | end 92 | 93 | end 94 | 95 | describe "#drop" do 96 | it "should raise an error for unknown database" do 97 | expect { 98 | subject.drop "unknown_database" 99 | }.to raise_error(Apartment::TenantNotFound) 100 | end 101 | 102 | context "numeric database names" do 103 | let(:db){ 1234 } 104 | 105 | it "should be able to drop them" do 106 | subject.create(db) 107 | expect { 108 | subject.drop(db) 109 | }.to_not raise_error 110 | expect(tenant_names).not_to include(db.to_s) 111 | end 112 | 113 | after { subject.drop(db) rescue nil } 114 | end 115 | end 116 | 117 | describe "#switch" do 118 | it "connects and resets" do 119 | subject.switch(schema1) do 120 | expect(connection.schema_search_path).to start_with %{"#{schema1}"} 121 | end 122 | 123 | expect(connection.schema_search_path).to start_with %{"#{public_schema}"} 124 | end 125 | end 126 | 127 | describe "#reset" do 128 | it "should reset connection" do 129 | subject.switch!(schema1) 130 | subject.reset 131 | expect(connection.schema_search_path).to start_with %{"#{public_schema}"} 132 | end 133 | 134 | context "with default_schema", :default_schema => true do 135 | it "should reset to the default schema" do 136 | subject.switch!(schema1) 137 | subject.reset 138 | expect(connection.schema_search_path).to start_with %{"#{default_schema}"} 139 | end 140 | end 141 | 142 | context "persistent_schemas", :persistent_schemas => true do 143 | before do 144 | subject.switch!(schema1) 145 | subject.reset 146 | end 147 | 148 | it "maintains the persistent schemas in the schema_search_path" do 149 | expect(connection.schema_search_path).to end_with persistent_schemas.map { |schema| %{"#{schema}"} }.join(', ') 150 | end 151 | 152 | context "with default_schema", :default_schema => true do 153 | it "prioritizes the switched schema to front of schema_search_path" do 154 | subject.reset # need to re-call this as the default_schema wasn't set at the time that the above reset ran 155 | expect(connection.schema_search_path).to start_with %{"#{default_schema}"} 156 | end 157 | end 158 | end 159 | end 160 | 161 | describe "#switch!" do 162 | it "should connect to new schema" do 163 | subject.switch!(schema1) 164 | expect(connection.schema_search_path).to start_with %{"#{schema1}"} 165 | end 166 | 167 | it "should reset connection if database is nil" do 168 | subject.switch! 169 | expect(connection.schema_search_path).to eq(%{"#{public_schema}"}) 170 | end 171 | 172 | it "should raise an error if schema is invalid" do 173 | expect { 174 | subject.switch! 'unknown_schema' 175 | }.to raise_error(Apartment::TenantNotFound) 176 | end 177 | 178 | context "numeric databases" do 179 | let(:db){ 1234 } 180 | 181 | it "should connect to them" do 182 | subject.create(db) 183 | expect { 184 | subject.switch!(db) 185 | }.to_not raise_error 186 | 187 | expect(connection.schema_search_path).to start_with %{"#{db.to_s}"} 188 | end 189 | 190 | after{ subject.drop(db) } 191 | end 192 | 193 | describe "with default_schema specified", :default_schema => true do 194 | before do 195 | subject.switch!(schema1) 196 | end 197 | 198 | it "should switch out the default schema rather than public" do 199 | expect(connection.schema_search_path).not_to include default_schema 200 | end 201 | 202 | it "should still switch to the switched schema" do 203 | expect(connection.schema_search_path).to start_with %{"#{schema1}"} 204 | end 205 | end 206 | 207 | context "persistent_schemas", :persistent_schemas => true do 208 | 209 | before{ subject.switch!(schema1) } 210 | 211 | it "maintains the persistent schemas in the schema_search_path" do 212 | expect(connection.schema_search_path).to end_with persistent_schemas.map { |schema| %{"#{schema}"} }.join(', ') 213 | end 214 | 215 | it "prioritizes the switched schema to front of schema_search_path" do 216 | expect(connection.schema_search_path).to start_with %{"#{schema1}"} 217 | end 218 | end 219 | end 220 | 221 | describe "#current" do 222 | it "should return the current schema name" do 223 | subject.switch!(schema1) 224 | expect(subject.current).to eq(schema1) 225 | end 226 | 227 | context "persistent_schemas", :persistent_schemas => true do 228 | it "should exlude persistent_schemas" do 229 | subject.switch!(schema1) 230 | expect(subject.current).to eq(schema1) 231 | end 232 | end 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /spec/integration/apartment_rake_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rake' 3 | 4 | describe "apartment rake tasks", database: :postgresql do 5 | 6 | before do 7 | @rake = Rake::Application.new 8 | Rake.application = @rake 9 | Dummy::Application.load_tasks 10 | 11 | # rails tasks running F up the schema... 12 | Rake::Task.define_task('db:migrate') 13 | Rake::Task.define_task('db:seed') 14 | Rake::Task.define_task('db:rollback') 15 | Rake::Task.define_task('db:migrate:up') 16 | Rake::Task.define_task('db:migrate:down') 17 | Rake::Task.define_task('db:migrate:redo') 18 | 19 | Apartment.configure do |config| 20 | config.use_schemas = true 21 | config.excluded_models = ["Company"] 22 | config.tenant_names = lambda{ Company.pluck(:database) } 23 | end 24 | Apartment::Tenant.reload!(config) 25 | 26 | # fix up table name of shared/excluded models 27 | Company.table_name = 'public.companies' 28 | end 29 | 30 | after { Rake.application = nil } 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){ db_names.length } 37 | 38 | before do 39 | db_names.collect do |db_name| 40 | Apartment::Tenant.create(db_name) 41 | Company.create :database => db_name 42 | end 43 | end 44 | 45 | after do 46 | db_names.each{ |db| Apartment::Tenant.drop(db) } 47 | Company.delete_all 48 | end 49 | 50 | context "with ActiveRecord below 5.2.0" do 51 | before do 52 | allow(ActiveRecord::Migrator).to receive(:migrations_paths) { %w(spec/dummy/db/migrate) } 53 | allow(Apartment::Migrator).to receive(:activerecord_below_5_2?) { true } 54 | end 55 | 56 | describe "#migrate" do 57 | it "should migrate all databases" do 58 | expect(ActiveRecord::Migrator).to receive(:migrate).exactly(company_count).times 59 | 60 | @rake['apartment:migrate'].invoke 61 | end 62 | end 63 | 64 | describe "#rollback" do 65 | it "should rollback all dbs" do 66 | expect(ActiveRecord::Migrator).to receive(:rollback).exactly(company_count).times 67 | 68 | @rake['apartment:rollback'].invoke 69 | end 70 | end 71 | end 72 | 73 | context "with ActiveRecord above or equal to 5.2.0" do 74 | let(:migration_context_double) { double(:migration_context) } 75 | 76 | before do 77 | allow(Apartment::Migrator).to receive(:activerecord_below_5_2?) { false } 78 | end 79 | 80 | describe "#migrate" do 81 | it "should migrate all databases" do 82 | allow(ActiveRecord::Base.connection).to receive(:migration_context) { migration_context_double } 83 | expect(migration_context_double).to receive(:migrate).exactly(company_count).times 84 | 85 | @rake['apartment:migrate'].invoke 86 | end 87 | end 88 | 89 | describe "#rollback" do 90 | it "should rollback all dbs" do 91 | allow(ActiveRecord::Base.connection).to receive(:migration_context) { migration_context_double } 92 | expect(migration_context_double).to receive(:rollback).exactly(company_count).times 93 | 94 | @rake['apartment:rollback'].invoke 95 | end 96 | end 97 | end 98 | 99 | describe "apartment:seed" do 100 | it "should seed all databases" do 101 | expect(Apartment::Tenant).to receive(:seed).exactly(company_count).times 102 | 103 | @rake['apartment:seed'].invoke 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/integration/query_caching_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'query caching' do 4 | describe 'when use_schemas = true' do 5 | let(:db_names) { [db1, db2] } 6 | 7 | before do 8 | Apartment.configure do |config| 9 | config.excluded_models = ["Company"] 10 | config.tenant_names = lambda{ Company.pluck(:database) } 11 | config.use_schemas = true 12 | end 13 | 14 | Apartment::Tenant.reload!(config) 15 | 16 | db_names.each do |db_name| 17 | Apartment::Tenant.create(db_name) 18 | Company.create database: db_name 19 | end 20 | end 21 | 22 | after do 23 | db_names.each{ |db| Apartment::Tenant.drop(db) } 24 | Apartment::Tenant.reset 25 | Company.delete_all 26 | end 27 | 28 | it 'clears the ActiveRecord::QueryCache after switching databases' do 29 | db_names.each do |db_name| 30 | Apartment::Tenant.switch! db_name 31 | User.create! name: db_name 32 | end 33 | 34 | ActiveRecord::Base.connection.enable_query_cache! 35 | 36 | Apartment::Tenant.switch! db_names.first 37 | expect(User.find_by_name(db_names.first).name).to eq(db_names.first) 38 | 39 | Apartment::Tenant.switch! db_names.last 40 | expect(User.find_by_name(db_names.first)).to be_nil 41 | end 42 | end 43 | 44 | describe 'when use_schemas = false' do 45 | let(:db_name) { db1 } 46 | 47 | before do 48 | Apartment.configure do |config| 49 | config.excluded_models = ["Company"] 50 | config.tenant_names = lambda{ Company.pluck(:database) } 51 | config.use_schemas = false 52 | end 53 | 54 | Apartment::Tenant.reload!(config) 55 | 56 | Apartment::Tenant.create(db_name) 57 | Company.create database: db_name 58 | end 59 | 60 | after do 61 | # Avoid cannot drop the currently open database. Maybe there is a better way to handle this. 62 | Apartment::Tenant.switch! 'template1' 63 | 64 | Apartment::Tenant.drop(db_name) 65 | Apartment::Tenant.reset 66 | Company.delete_all 67 | end 68 | 69 | it "configuration value is kept after switching databases" do 70 | ActiveRecord::Base.connection.enable_query_cache! 71 | 72 | Apartment::Tenant.switch! db_name 73 | expect(Apartment.connection.query_cache_enabled).to be true 74 | 75 | ActiveRecord::Base.connection.disable_query_cache! 76 | 77 | Apartment::Tenant.switch! db_name 78 | expect(Apartment.connection.query_cache_enabled).to be false 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/integration/use_within_an_engine_spec.rb: -------------------------------------------------------------------------------- 1 | describe 'using apartment within an engine' do 2 | 3 | before do 4 | engine_path = Pathname.new(File.expand_path('../../dummy_engine', __FILE__)) 5 | require engine_path.join('test/dummy/config/application') 6 | @rake = Rake::Application.new 7 | Rake.application = @rake 8 | stub_const 'APP_RAKEFILE', engine_path.join('test/dummy/Rakefile') 9 | load 'rails/tasks/engine.rake' 10 | end 11 | 12 | it 'sucessfully runs rake db:migrate in the engine root' do 13 | expect{ Rake::Task['db:migrate'].invoke }.to_not raise_error 14 | end 15 | 16 | it 'sucessfully runs rake app:db:migrate in the engine root' do 17 | expect{ Rake::Task['app:db:migrate'].invoke }.to_not raise_error 18 | end 19 | 20 | context 'when Apartment.db_migrate_tenants is false' do 21 | it 'should not enhance tasks' do 22 | Apartment.db_migrate_tenants = false 23 | expect(Apartment::RakeTaskEnhancer).to_not receive(:enhance_task).with('db:migrate') 24 | Rake::Task['db:migrate'].invoke 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/schemas/v1.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 => 0) do 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/schemas/v2.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 => 20110613152810) do 15 | 16 | create_table "companies", :force => true do |t| 17 | t.boolean "dummy" 18 | t.string "database" 19 | end 20 | 21 | create_table "delayed_jobs", :force => true do |t| 22 | t.integer "priority", :default => 0 23 | t.integer "attempts", :default => 0 24 | t.text "handler" 25 | t.text "last_error" 26 | t.datetime "run_at" 27 | t.datetime "locked_at" 28 | t.datetime "failed_at" 29 | t.string "locked_by" 30 | t.datetime "created_at" 31 | t.datetime "updated_at" 32 | t.string "queue" 33 | end 34 | 35 | add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority" 36 | 37 | create_table "users", :force => true do |t| 38 | t.string "name" 39 | t.datetime "birthdate" 40 | t.string "sex" 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /spec/schemas/v3.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 | -------------------------------------------------------------------------------- /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 | 8 | # Loading dummy applications affects table_name of each excluded models 9 | # defined in `spec/dummy/config/initializers/apartment.rb`. 10 | # To make them pristine, we need to execute below lines. 11 | Apartment.excluded_models.each do |model| 12 | klass = model.constantize 13 | 14 | Apartment.connection_class.remove_connection(klass) 15 | klass.clear_all_connections! 16 | klass.reset_table_name 17 | end 18 | 19 | require "rspec/rails" 20 | require 'capybara/rspec' 21 | require 'capybara/rails' 22 | 23 | begin 24 | require 'pry' 25 | silence_warnings{ IRB = Pry } 26 | rescue LoadError 27 | end 28 | 29 | ActionMailer::Base.delivery_method = :test 30 | ActionMailer::Base.perform_deliveries = true 31 | ActionMailer::Base.default_url_options[:host] = "test.com" 32 | 33 | Rails.backtrace_cleaner.remove_silencers! 34 | 35 | # Load support files 36 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 37 | 38 | RSpec.configure do |config| 39 | 40 | config.include RSpec::Integration::CapybaraSessions, type: :request 41 | config.include Apartment::Spec::Setup 42 | 43 | # Somewhat brutal hack so that rails 4 postgres extensions don't modify this file 44 | config.after(:all) do 45 | `git checkout -- spec/dummy/db/schema.rb` 46 | end 47 | 48 | # rspec-rails 3 will no longer automatically infer an example group's spec type 49 | # from the file location. You can explicitly opt-in to the feature using this 50 | # config option. 51 | # To explicitly tag specs without using automatic inference, set the `:type` 52 | # metadata manually: 53 | # 54 | # describe ThingsController, :type => :controller do 55 | # # Equivalent to being in spec/controllers 56 | # end 57 | config.infer_spec_type_from_file_location! 58 | end 59 | 60 | # Load shared examples, must happen after configure for RSpec 3 61 | Dir["#{File.dirname(__FILE__)}/examples/**/*.rb"].each { |f| require f } 62 | -------------------------------------------------------------------------------- /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_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(version = 3) 29 | file = File.expand_path("../../schemas/v#{version}.rb", __FILE__) 30 | 31 | silence_warnings{ load(file) } 32 | end 33 | 34 | def migrate 35 | ActiveRecord::Migrator.migrate(Rails.root + ActiveRecord::Migrator.migrations_path) 36 | end 37 | 38 | def rollback 39 | ActiveRecord::Migrator.rollback(Rails.root + ActiveRecord::Migrator.migrations_path) 40 | end 41 | 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /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/support/config.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Apartment 4 | module Test 5 | 6 | def self.config 7 | @config ||= YAML.load(ERB.new(IO.read('spec/config/database.yml')).result) 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /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: db1).as_null_object } 21 | let(:company2) { mock_model(Company, database: db2).as_null_object } 22 | 23 | let(:api) { Apartment::Tenant } 24 | 25 | before do 26 | Apartment.reset # reset all config 27 | Apartment.seed_after_create = false 28 | Apartment.use_schemas = true 29 | api.reload!(config) 30 | api.create(db1) 31 | api.create(db2) 32 | end 33 | 34 | after do 35 | api.drop(db1) 36 | api.drop(db2) 37 | end 38 | end 39 | 40 | shared_context "persistent_schemas", :persistent_schemas => true do 41 | let(:persistent_schemas){ ['hstore', 'postgis'] } 42 | 43 | before do 44 | persistent_schemas.map{|schema| subject.create(schema) } 45 | Apartment.persistent_schemas = persistent_schemas 46 | end 47 | 48 | after do 49 | Apartment.persistent_schemas = [] 50 | persistent_schemas.map{|schema| subject.drop(schema) } 51 | end 52 | end -------------------------------------------------------------------------------- /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 | extend ActiveSupport::Concern 11 | 12 | included do 13 | before do 14 | subject.create(db1) 15 | subject.create(db2) 16 | end 17 | 18 | after do 19 | # Reset before dropping (can't drop a db you're connected to) 20 | subject.reset 21 | 22 | # sometimes we manually drop these schemas in testing, don't care if we can't drop, hence rescue 23 | subject.drop(db1) rescue true 24 | subject.drop(db2) rescue true 25 | end 26 | end 27 | 28 | %w{subject tenant_names default_tenant}.each do |method| 29 | define_method method do 30 | raise "You must define a `#{method}` method in your host group" 31 | end unless defined?(method) 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/setup.rb: -------------------------------------------------------------------------------- 1 | module Apartment 2 | module Spec 3 | module Setup 4 | 5 | def self.included(base) 6 | base.instance_eval do 7 | let(:db1){ Apartment::Test.next_db } 8 | let(:db2){ Apartment::Test.next_db } 9 | let(:connection){ ActiveRecord::Base.connection } 10 | 11 | # This around ensures that we run these hooks before and after 12 | # any before/after hooks defined in individual tests 13 | # Otherwise these actually get run after test defined hooks 14 | around(:each) do |example| 15 | 16 | def config 17 | db = RSpec.current_example.metadata.fetch(:database, :postgresql) 18 | 19 | Apartment::Test.config['connections'][db.to_s].symbolize_keys 20 | end 21 | 22 | # before 23 | Apartment::Tenant.reload!(config) 24 | ActiveRecord::Base.establish_connection config 25 | 26 | example.run 27 | 28 | # after 29 | Rails.configuration.database_configuration = {} 30 | ActiveRecord::Base.clear_all_connections! 31 | 32 | Apartment.excluded_models.each do |model| 33 | klass = model.constantize 34 | 35 | Apartment.connection_class.remove_connection(klass) 36 | klass.clear_all_connections! 37 | klass.reset_table_name 38 | end 39 | Apartment.reset 40 | Apartment::Tenant.reload! 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/tasks/apartment_rake_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'rake' 3 | require 'apartment/migrator' 4 | require 'apartment/tenant' 5 | 6 | describe "apartment rake tasks" do 7 | 8 | before do 9 | @rake = Rake::Application.new 10 | Rake.application = @rake 11 | load 'tasks/apartment.rake' 12 | # stub out rails tasks 13 | Rake::Task.define_task('db:migrate') 14 | Rake::Task.define_task('db:seed') 15 | Rake::Task.define_task('db:rollback') 16 | Rake::Task.define_task('db:migrate:up') 17 | Rake::Task.define_task('db:migrate:down') 18 | Rake::Task.define_task('db:migrate:redo') 19 | end 20 | 21 | after do 22 | Rake.application = nil 23 | ENV['VERSION'] = nil # linux users reported env variable carrying on between tests 24 | end 25 | 26 | after(:all) do 27 | Apartment::Test.load_schema 28 | end 29 | 30 | let(:version){ '1234' } 31 | 32 | context 'database migration' do 33 | 34 | let(:tenant_names){ 3.times.map{ Apartment::Test.next_db } } 35 | let(:tenant_count){ tenant_names.length } 36 | 37 | before do 38 | allow(Apartment).to receive(:tenant_names).and_return tenant_names 39 | end 40 | 41 | describe "apartment:migrate" do 42 | before do 43 | allow(ActiveRecord::Migrator).to receive(:migrate) # don't care about this 44 | end 45 | 46 | it "should migrate public and all multi-tenant dbs" do 47 | expect(Apartment::Migrator).to receive(:migrate).exactly(tenant_count).times 48 | @rake['apartment:migrate'].invoke 49 | end 50 | end 51 | 52 | describe "apartment:migrate:up" do 53 | 54 | context "without a version" do 55 | before do 56 | ENV['VERSION'] = nil 57 | end 58 | 59 | it "requires a version to migrate to" do 60 | expect{ 61 | @rake['apartment:migrate:up'].invoke 62 | }.to raise_error("VERSION is required") 63 | end 64 | end 65 | 66 | context "with version" do 67 | 68 | before do 69 | ENV['VERSION'] = version 70 | end 71 | 72 | it "migrates up to a specific version" do 73 | expect(Apartment::Migrator).to receive(:run).with(:up, anything, version.to_i).exactly(tenant_count).times 74 | @rake['apartment:migrate:up'].invoke 75 | end 76 | end 77 | end 78 | 79 | describe "apartment:migrate:down" do 80 | 81 | context "without a version" do 82 | before do 83 | ENV['VERSION'] = nil 84 | end 85 | 86 | it "requires a version to migrate to" do 87 | expect{ 88 | @rake['apartment:migrate:down'].invoke 89 | }.to raise_error("VERSION is required") 90 | end 91 | end 92 | 93 | context "with version" do 94 | 95 | before do 96 | ENV['VERSION'] = version 97 | end 98 | 99 | it "migrates up to a specific version" do 100 | expect(Apartment::Migrator).to receive(:run).with(:down, anything, version.to_i).exactly(tenant_count).times 101 | @rake['apartment:migrate:down'].invoke 102 | end 103 | end 104 | end 105 | 106 | describe "apartment:rollback" do 107 | let(:step){ '3' } 108 | 109 | it "should rollback dbs" do 110 | expect(Apartment::Migrator).to receive(:rollback).exactly(tenant_count).times 111 | @rake['apartment:rollback'].invoke 112 | end 113 | 114 | it "should rollback dbs STEP amt" do 115 | expect(Apartment::Migrator).to receive(:rollback).with(anything, step.to_i).exactly(tenant_count).times 116 | ENV['STEP'] = step 117 | @rake['apartment:rollback'].invoke 118 | end 119 | end 120 | 121 | describe "apartment:drop" do 122 | it "should migrate public and all multi-tenant dbs" do 123 | expect(Apartment::Tenant).to receive(:drop).exactly(tenant_count).times 124 | @rake['apartment:drop'].invoke 125 | end 126 | end 127 | 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /spec/tenant_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Apartment::Tenant do 4 | context "using mysql", database: :mysql do 5 | 6 | before { subject.reload!(config) } 7 | 8 | describe "#adapter" do 9 | it "should load mysql adapter" do 10 | subject.adapter 11 | expect(Apartment::Adapters::Mysql2Adapter).to be_a(Class) 12 | end 13 | end 14 | 15 | # TODO this doesn't belong here, but there aren't integration tests currently for mysql 16 | # where to put??? 17 | describe "exception recovery", :type => :request do 18 | before do 19 | subject.create db1 20 | end 21 | after{ subject.drop db1 } 22 | 23 | # it "should recover from incorrect database" do 24 | # session = Capybara::Session.new(:rack_test, Capybara.app) 25 | # session.visit("http://#{db1}.com") 26 | # expect { 27 | # session.visit("http://this-database-should-not-exist.com") 28 | # }.to raise_error 29 | # session.visit("http://#{db1}.com") 30 | # end 31 | end 32 | 33 | # TODO re-organize these tests 34 | context "with prefix and schemas" do 35 | describe "#create" do 36 | before do 37 | Apartment.configure do |config| 38 | config.prepend_environment = true 39 | config.use_schemas = true 40 | end 41 | 42 | subject.reload!(config) 43 | end 44 | 45 | after { subject.drop "db_with_prefix" rescue nil } 46 | 47 | it "should create a new database" do 48 | subject.create "db_with_prefix" 49 | end 50 | end 51 | end 52 | end 53 | 54 | context "using postgresql", database: :postgresql do 55 | before do 56 | Apartment.use_schemas = true 57 | subject.reload!(config) 58 | end 59 | 60 | describe "#adapter" do 61 | it "should load postgresql adapter" do 62 | expect(subject.adapter).to be_a(Apartment::Adapters::PostgresqlSchemaAdapter) 63 | end 64 | 65 | it "raises exception with invalid adapter specified" do 66 | subject.reload!(config.merge(adapter: 'unknown')) 67 | 68 | expect { 69 | Apartment::Tenant.adapter 70 | }.to raise_error(RuntimeError) 71 | end 72 | 73 | context "threadsafety" do 74 | before { subject.create db1 } 75 | after { subject.drop db1 } 76 | 77 | it 'has a threadsafe adapter' do 78 | subject.switch!(db1) 79 | thread = Thread.new { expect(subject.current).to eq(Apartment.default_tenant) } 80 | thread.join 81 | expect(subject.current).to eq(db1) 82 | end 83 | end 84 | end 85 | 86 | # TODO above spec are also with use_schemas=true 87 | context "with schemas" do 88 | before do 89 | Apartment.configure do |config| 90 | config.excluded_models = [] 91 | config.use_schemas = true 92 | config.seed_after_create = true 93 | end 94 | subject.create db1 95 | end 96 | 97 | after{ subject.drop db1 } 98 | 99 | describe "#create" do 100 | it "should seed data" do 101 | subject.switch! db1 102 | expect(User.count).to be > 0 103 | end 104 | end 105 | 106 | describe "#switch!" do 107 | 108 | let(:x){ rand(3) } 109 | 110 | context "creating models" do 111 | 112 | before{ subject.create db2 } 113 | after{ subject.drop db2 } 114 | 115 | it "should create a model instance in the current schema" do 116 | subject.switch! db2 117 | db2_count = User.count + x.times{ User.create } 118 | 119 | subject.switch! db1 120 | db_count = User.count + x.times{ User.create } 121 | 122 | subject.switch! db2 123 | expect(User.count).to eq(db2_count) 124 | 125 | subject.switch! db1 126 | expect(User.count).to eq(db_count) 127 | end 128 | end 129 | 130 | context "with excluded models" do 131 | 132 | before do 133 | Apartment.configure do |config| 134 | config.excluded_models = ["Company"] 135 | end 136 | subject.init 137 | end 138 | 139 | after do 140 | # Apartment::Tenant.init creates per model connection. 141 | # Remove the connection after testing not to unintentionally keep the connection across tests. 142 | Apartment.excluded_models.each do |excluded_model| 143 | excluded_model.constantize.remove_connection 144 | end 145 | end 146 | 147 | it "should create excluded models in public schema" do 148 | subject.reset # ensure we're on public schema 149 | count = Company.count + x.times{ Company.create } 150 | 151 | subject.switch! db1 152 | x.times{ Company.create } 153 | expect(Company.count).to eq(count + x) 154 | subject.reset 155 | expect(Company.count).to eq(count + x) 156 | end 157 | end 158 | end 159 | end 160 | 161 | context "seed paths" do 162 | before do 163 | Apartment.configure do |config| 164 | config.excluded_models = [] 165 | config.use_schemas = true 166 | config.seed_after_create = true 167 | end 168 | end 169 | 170 | after{ subject.drop db1 } 171 | 172 | it 'should seed from default path' do 173 | subject.create db1 174 | subject.switch! db1 175 | expect(User.count).to eq(3) 176 | expect(User.first.name).to eq('Some User 0') 177 | end 178 | 179 | it 'should seed from custom path' do 180 | Apartment.configure do |config| 181 | config.seed_data_file = "#{Rails.root}/db/seeds/import.rb" 182 | end 183 | subject.create db1 184 | subject.switch! db1 185 | expect(User.count).to eq(6) 186 | expect(User.first.name).to eq('Different User 0') 187 | end 188 | end 189 | end 190 | end 191 | -------------------------------------------------------------------------------- /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 | let(:seed_data_file_path){ "#{Rails.root}/db/seeds/import.rb" } 9 | 10 | def tenant_names_from_array(names) 11 | names.each_with_object({}) do |tenant, hash| 12 | hash[tenant] = Apartment.connection_config 13 | end.with_indifferent_access 14 | end 15 | 16 | it "should yield the Apartment object" do 17 | Apartment.configure do |config| 18 | config.excluded_models = [] 19 | expect(config).to eq(Apartment) 20 | end 21 | end 22 | 23 | it "should set excluded models" do 24 | Apartment.configure do |config| 25 | config.excluded_models = excluded_models 26 | end 27 | expect(Apartment.excluded_models).to eq(excluded_models) 28 | end 29 | 30 | it "should set use_schemas" do 31 | Apartment.configure do |config| 32 | config.excluded_models = [] 33 | config.use_schemas = false 34 | end 35 | expect(Apartment.use_schemas).to be false 36 | end 37 | 38 | it "should set seed_data_file" do 39 | Apartment.configure do |config| 40 | config.seed_data_file = seed_data_file_path 41 | end 42 | expect(Apartment.seed_data_file).to eq(seed_data_file_path) 43 | end 44 | 45 | it "should set seed_after_create" do 46 | Apartment.configure do |config| 47 | config.excluded_models = [] 48 | config.seed_after_create = true 49 | end 50 | expect(Apartment.seed_after_create).to be true 51 | end 52 | 53 | context "databases" do 54 | let(:users_conf_hash) { { port: 5444 } } 55 | 56 | before do 57 | Apartment.configure do |config| 58 | config.tenant_names = tenant_names 59 | end 60 | end 61 | 62 | context "tenant_names as string array" do 63 | let(:tenant_names) { ['users', 'companies'] } 64 | 65 | it "should return object if it doesnt respond_to call" do 66 | expect(Apartment.tenant_names).to eq(tenant_names_from_array(tenant_names).keys) 67 | end 68 | 69 | it "should set tenants_with_config" do 70 | expect(Apartment.tenants_with_config).to eq(tenant_names_from_array(tenant_names)) 71 | end 72 | end 73 | 74 | context "tenant_names as proc returning an array" do 75 | let(:tenant_names) { lambda { ['users', 'companies'] } } 76 | 77 | it "should return object if it doesnt respond_to call" do 78 | expect(Apartment.tenant_names).to eq(tenant_names_from_array(tenant_names.call).keys) 79 | end 80 | 81 | it "should set tenants_with_config" do 82 | expect(Apartment.tenants_with_config).to eq(tenant_names_from_array(tenant_names.call)) 83 | end 84 | end 85 | 86 | context "tenant_names as Hash" do 87 | let(:tenant_names) { { users: users_conf_hash }.with_indifferent_access } 88 | 89 | it "should return object if it doesnt respond_to call" do 90 | expect(Apartment.tenant_names).to eq(tenant_names.keys) 91 | end 92 | 93 | it "should set tenants_with_config" do 94 | expect(Apartment.tenants_with_config).to eq(tenant_names) 95 | end 96 | end 97 | 98 | context "tenant_names as proc returning a Hash" do 99 | let(:tenant_names) { lambda { { users: users_conf_hash }.with_indifferent_access } } 100 | 101 | it "should return object if it doesnt respond_to call" do 102 | expect(Apartment.tenant_names).to eq(tenant_names.call.keys) 103 | end 104 | 105 | it "should set tenants_with_config" do 106 | expect(Apartment.tenants_with_config).to eq(tenant_names.call) 107 | end 108 | end 109 | end 110 | 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /spec/unit/elevators/domain_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/domain' 3 | 4 | describe Apartment::Elevators::Domain do 5 | 6 | subject(:elevator){ described_class.new(Proc.new{}) } 7 | 8 | describe "#parse_tenant_name" do 9 | it "parses the host for a domain name" do 10 | request = ActionDispatch::Request.new('HTTP_HOST' => 'example.com') 11 | expect(elevator.parse_tenant_name(request)).to eq('example') 12 | end 13 | 14 | it "ignores a www prefix and domain suffix" do 15 | request = ActionDispatch::Request.new('HTTP_HOST' => 'www.example.bc.ca') 16 | expect(elevator.parse_tenant_name(request)).to eq('example') 17 | end 18 | 19 | it "returns nil if there is no host" do 20 | request = ActionDispatch::Request.new('HTTP_HOST' => '') 21 | expect(elevator.parse_tenant_name(request)).to be_nil 22 | end 23 | end 24 | 25 | describe "#call" do 26 | it "switches to the proper tenant" do 27 | expect(Apartment::Tenant).to receive(:switch).with('example') 28 | 29 | elevator.call('HTTP_HOST' => 'www.example.com') 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/elevators/first_subdomain_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/first_subdomain' 3 | 4 | describe Apartment::Elevators::FirstSubdomain do 5 | describe "subdomain" do 6 | subject { described_class.new("test").parse_tenant_name(request) } 7 | let(:request) { double(:request, :host => "#{subdomain}.example.com") } 8 | 9 | context "one subdomain" do 10 | let(:subdomain) { "test" } 11 | it { is_expected.to eq("test") } 12 | end 13 | 14 | context "nested subdomains" do 15 | let(:subdomain) { "test1.test2" } 16 | it { is_expected.to eq("test1") } 17 | end 18 | 19 | context "no subdomain" do 20 | let(:subdomain) { nil } 21 | it { is_expected.to eq(nil) } 22 | end 23 | end 24 | end -------------------------------------------------------------------------------- /spec/unit/elevators/generic_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/generic' 3 | 4 | describe Apartment::Elevators::Generic do 5 | 6 | class MyElevator < described_class 7 | def parse_tenant_name(*) 8 | 'tenant2' 9 | end 10 | end 11 | 12 | subject(:elevator){ described_class.new(Proc.new{}) } 13 | 14 | describe "#call" do 15 | it "calls the processor if given" do 16 | elevator = described_class.new(Proc.new{}, Proc.new{'tenant1'}) 17 | 18 | expect(Apartment::Tenant).to receive(:switch).with('tenant1') 19 | 20 | elevator.call('HTTP_HOST' => 'foo.bar.com') 21 | end 22 | 23 | it "raises if parse_tenant_name not implemented" do 24 | expect { 25 | elevator.call('HTTP_HOST' => 'foo.bar.com') 26 | }.to raise_error(RuntimeError) 27 | end 28 | 29 | it "switches to the parsed db_name" do 30 | elevator = MyElevator.new(Proc.new{}) 31 | 32 | expect(Apartment::Tenant).to receive(:switch).with('tenant2') 33 | 34 | elevator.call('HTTP_HOST' => 'foo.bar.com') 35 | end 36 | 37 | it "calls the block implementation of `switch`" do 38 | elevator = MyElevator.new(Proc.new{}, Proc.new{'tenant2'}) 39 | 40 | expect(Apartment::Tenant).to receive(:switch).with('tenant2').and_yield 41 | elevator.call('HTTP_HOST' => 'foo.bar.com') 42 | end 43 | 44 | it "does not call `switch` if no database given" do 45 | app = Proc.new{} 46 | elevator = MyElevator.new(app, Proc.new{}) 47 | 48 | expect(Apartment::Tenant).not_to receive(:switch) 49 | expect(app).to receive :call 50 | 51 | elevator.call('HTTP_HOST' => 'foo.bar.com') 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/unit/elevators/host_hash_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/host_hash' 3 | 4 | describe Apartment::Elevators::HostHash do 5 | 6 | subject(:elevator){ Apartment::Elevators::HostHash.new(Proc.new{}, 'example.com' => 'example_tenant') } 7 | 8 | describe "#parse_tenant_name" do 9 | it "parses the host for a domain name" do 10 | request = ActionDispatch::Request.new('HTTP_HOST' => 'example.com') 11 | expect(elevator.parse_tenant_name(request)).to eq('example_tenant') 12 | end 13 | 14 | it "raises TenantNotFound exception if there is no host" do 15 | request = ActionDispatch::Request.new('HTTP_HOST' => '') 16 | expect { elevator.parse_tenant_name(request) }.to raise_error(Apartment::TenantNotFound) 17 | end 18 | 19 | it "raises TenantNotFound exception if there is no database associated to current host" do 20 | request = ActionDispatch::Request.new('HTTP_HOST' => 'example2.com') 21 | expect { elevator.parse_tenant_name(request) }.to raise_error(Apartment::TenantNotFound) 22 | end 23 | end 24 | 25 | describe "#call" do 26 | it "switches to the proper tenant" do 27 | expect(Apartment::Tenant).to receive(:switch).with('example_tenant') 28 | 29 | elevator.call('HTTP_HOST' => 'example.com') 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/elevators/host_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/host' 3 | 4 | describe Apartment::Elevators::Host do 5 | 6 | subject(:elevator){ described_class.new(Proc.new{}) } 7 | 8 | describe "#parse_tenant_name" do 9 | 10 | it "should return nil when no host" do 11 | request = ActionDispatch::Request.new('HTTP_HOST' => '') 12 | expect(elevator.parse_tenant_name(request)).to be_nil 13 | end 14 | 15 | context "assuming no ignored_first_subdomains" do 16 | before { allow(described_class).to receive(:ignored_first_subdomains).and_return([]) } 17 | 18 | context "with 3 parts" do 19 | it "should return the whole host" do 20 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.com') 21 | expect(elevator.parse_tenant_name(request)).to eq('foo.bar.com') 22 | end 23 | end 24 | 25 | context "with 6 parts" do 26 | it "should return the whole host" do 27 | request = ActionDispatch::Request.new('HTTP_HOST' => 'one.two.three.foo.bar.com') 28 | expect(elevator.parse_tenant_name(request)).to eq('one.two.three.foo.bar.com') 29 | end 30 | end 31 | end 32 | 33 | context "assuming ignored_first_subdomains is set" do 34 | before { allow(described_class).to receive(:ignored_first_subdomains).and_return(%w{www foo}) } 35 | 36 | context "with 3 parts" do 37 | it "should return host without www" do 38 | request = ActionDispatch::Request.new('HTTP_HOST' => 'www.bar.com') 39 | expect(elevator.parse_tenant_name(request)).to eq('bar.com') 40 | end 41 | 42 | it "should return host without foo" do 43 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.com') 44 | expect(elevator.parse_tenant_name(request)).to eq('bar.com') 45 | end 46 | end 47 | 48 | context "with 6 parts" do 49 | it "should return host without www" do 50 | request = ActionDispatch::Request.new('HTTP_HOST' => 'www.one.two.three.foo.bar.com') 51 | expect(elevator.parse_tenant_name(request)).to eq('one.two.three.foo.bar.com') 52 | end 53 | 54 | it "should return host without www" do 55 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.one.two.three.bar.com') 56 | expect(elevator.parse_tenant_name(request)).to eq('one.two.three.bar.com') 57 | end 58 | end 59 | end 60 | 61 | context "assuming localhost" do 62 | it "should return localhost" do 63 | request = ActionDispatch::Request.new('HTTP_HOST' => 'localhost') 64 | expect(elevator.parse_tenant_name(request)).to eq('localhost') 65 | end 66 | end 67 | 68 | context "assuming ip address" do 69 | it "should return the ip address" do 70 | request = ActionDispatch::Request.new('HTTP_HOST' => '127.0.0.1') 71 | expect(elevator.parse_tenant_name(request)).to eq('127.0.0.1') 72 | end 73 | end 74 | end 75 | 76 | describe "#call" do 77 | it "switches to the proper tenant" do 78 | allow(described_class).to receive(:ignored_first_subdomains).and_return([]) 79 | expect(Apartment::Tenant).to receive(:switch).with('foo.bar.com') 80 | elevator.call('HTTP_HOST' => 'foo.bar.com') 81 | end 82 | 83 | it "ignores ignored_first_subdomains" do 84 | allow(described_class).to receive(:ignored_first_subdomains).and_return(%w{foo}) 85 | expect(Apartment::Tenant).to receive(:switch).with('bar.com') 86 | elevator.call('HTTP_HOST' => 'foo.bar.com') 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/unit/elevators/subdomain_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/elevators/subdomain' 3 | 4 | describe Apartment::Elevators::Subdomain do 5 | 6 | subject(:elevator){ described_class.new(Proc.new{}) } 7 | 8 | describe "#parse_tenant_name" do 9 | context "assuming one tld" do 10 | it "should parse subdomain" do 11 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.com') 12 | expect(elevator.parse_tenant_name(request)).to eq('foo') 13 | end 14 | 15 | it "should return nil when no subdomain" do 16 | request = ActionDispatch::Request.new('HTTP_HOST' => 'bar.com') 17 | expect(elevator.parse_tenant_name(request)).to be_nil 18 | end 19 | end 20 | 21 | context "assuming two tlds" do 22 | it "should parse subdomain in the third level domain" do 23 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.co.uk') 24 | expect(elevator.parse_tenant_name(request)).to eq("foo") 25 | end 26 | 27 | it "should return nil when no subdomain in the third level domain" do 28 | request = ActionDispatch::Request.new('HTTP_HOST' => 'bar.co.uk') 29 | expect(elevator.parse_tenant_name(request)).to be_nil 30 | end 31 | end 32 | 33 | context "assuming two subdomains" do 34 | it "should parse two subdomains in the two level domain" do 35 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.xyz.bar.com') 36 | expect(elevator.parse_tenant_name(request)).to eq("foo") 37 | end 38 | 39 | it "should parse two subdomains in the third level domain" do 40 | request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.xyz.bar.co.uk') 41 | expect(elevator.parse_tenant_name(request)).to eq("foo") 42 | end 43 | end 44 | 45 | context "assuming localhost" do 46 | it "should return nil for localhost" do 47 | request = ActionDispatch::Request.new('HTTP_HOST' => 'localhost') 48 | expect(elevator.parse_tenant_name(request)).to be_nil 49 | end 50 | end 51 | 52 | context "assuming ip address" do 53 | it "should return nil for an ip address" do 54 | request = ActionDispatch::Request.new('HTTP_HOST' => '127.0.0.1') 55 | expect(elevator.parse_tenant_name(request)).to be_nil 56 | end 57 | end 58 | end 59 | 60 | describe "#call" do 61 | it "switches to the proper tenant" do 62 | expect(Apartment::Tenant).to receive(:switch).with('tenant1') 63 | elevator.call('HTTP_HOST' => 'tenant1.example.com') 64 | end 65 | 66 | it "ignores excluded subdomains" do 67 | described_class.excluded_subdomains = %w{foo} 68 | 69 | expect(Apartment::Tenant).not_to receive(:switch) 70 | 71 | elevator.call('HTTP_HOST' => 'foo.bar.com') 72 | 73 | described_class.excluded_subdomains = nil 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/unit/migrator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'apartment/migrator' 3 | 4 | describe Apartment::Migrator do 5 | 6 | let(:tenant){ Apartment::Test.next_db } 7 | 8 | # Don't need a real switch here, just testing behaviour 9 | before { allow(Apartment::Tenant.adapter).to receive(:connect_to_new) } 10 | 11 | context "with ActiveRecord below 5.2.0", skip: ActiveRecord.version >= Gem::Version.new("5.2.0") do 12 | before do 13 | allow(ActiveRecord::Migrator).to receive(:migrations_paths) { %w(spec/dummy/db/migrate) } 14 | allow(Apartment::Migrator).to receive(:activerecord_below_5_2?) { true } 15 | end 16 | 17 | describe "::migrate" do 18 | it "switches and migrates" do 19 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 20 | expect(ActiveRecord::Migrator).to receive(:migrate) 21 | 22 | Apartment::Migrator.migrate(tenant) 23 | end 24 | end 25 | 26 | describe "::run" do 27 | it "switches and runs" do 28 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 29 | expect(ActiveRecord::Migrator).to receive(:run).with(:up, anything, 1234) 30 | 31 | Apartment::Migrator.run(:up, tenant, 1234) 32 | end 33 | end 34 | 35 | describe "::rollback" do 36 | it "switches and rolls back" do 37 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 38 | expect(ActiveRecord::Migrator).to receive(:rollback).with(anything, 2) 39 | 40 | Apartment::Migrator.rollback(tenant, 2) 41 | end 42 | end 43 | end 44 | 45 | context "with ActiveRecord above or equal to 5.2.0", skip: ActiveRecord.version < Gem::Version.new("5.2.0") do 46 | before do 47 | allow(Apartment::Migrator).to receive(:activerecord_below_5_2?) { false } 48 | end 49 | 50 | describe "::migrate" do 51 | it "switches and migrates" do 52 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 53 | expect_any_instance_of(ActiveRecord::MigrationContext).to receive(:migrate) 54 | 55 | Apartment::Migrator.migrate(tenant) 56 | end 57 | end 58 | 59 | describe "::run" do 60 | it "switches and runs" do 61 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 62 | expect_any_instance_of(ActiveRecord::MigrationContext).to receive(:run).with(:up, 1234) 63 | 64 | Apartment::Migrator.run(:up, tenant, 1234) 65 | end 66 | end 67 | 68 | describe "::rollback" do 69 | it "switches and rolls back" do 70 | expect(Apartment::Tenant).to receive(:switch).with(tenant).and_call_original 71 | expect_any_instance_of(ActiveRecord::MigrationContext).to receive(:rollback).with(2) 72 | 73 | Apartment::Migrator.rollback(tenant, 2) 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /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.configure do |config| 9 | config.excluded_models = ["Company"] 10 | config.use_schemas = true 11 | end 12 | Apartment::Tenant.reload!(config) 13 | Company.reset_table_name # ensure we're clean 14 | end 15 | 16 | subject{ Apartment::Reloader.new(double("Rack::Application", :call => nil)) } 17 | 18 | it "should initialize apartment when called" do 19 | expect(Company.table_name).not_to include('public.') 20 | subject.call(double('env')) 21 | expect(Company.table_name).to include('public.') 22 | end 23 | end 24 | end --------------------------------------------------------------------------------