dr-apartment 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +22 -0
  5. data/HISTORY.md +133 -0
  6. data/README.md +152 -0
  7. data/Rakefile +79 -0
  8. data/apartment.gemspec +32 -0
  9. data/lib/apartment.rb +69 -0
  10. data/lib/apartment/adapters/abstract_adapter.rb +176 -0
  11. data/lib/apartment/adapters/jdbcpostgresql_adapter.rb +115 -0
  12. data/lib/apartment/adapters/mysql_adapter.rb +18 -0
  13. data/lib/apartment/adapters/postgresql_adapter.rb +114 -0
  14. data/lib/apartment/console.rb +12 -0
  15. data/lib/apartment/database.rb +57 -0
  16. data/lib/apartment/delayed_job/active_record.rb +20 -0
  17. data/lib/apartment/delayed_job/enqueue.rb +20 -0
  18. data/lib/apartment/delayed_job/hooks.rb +25 -0
  19. data/lib/apartment/delayed_job/requirements.rb +23 -0
  20. data/lib/apartment/elevators/subdomain.rb +27 -0
  21. data/lib/apartment/migrator.rb +23 -0
  22. data/lib/apartment/railtie.rb +54 -0
  23. data/lib/apartment/reloader.rb +24 -0
  24. data/lib/apartment/version.rb +3 -0
  25. data/lib/tasks/apartment.rake +70 -0
  26. data/spec/adapters/mysql_adapter_spec.rb +36 -0
  27. data/spec/adapters/postgresql_adapter_spec.rb +137 -0
  28. data/spec/apartment_spec.rb +11 -0
  29. data/spec/config/database.yml +13 -0
  30. data/spec/dummy/Rakefile +7 -0
  31. data/spec/dummy/app/controllers/application_controller.rb +6 -0
  32. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  33. data/spec/dummy/app/models/company.rb +3 -0
  34. data/spec/dummy/app/models/user.rb +3 -0
  35. data/spec/dummy/app/views/application/index.html.erb +1 -0
  36. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  37. data/spec/dummy/config.ru +4 -0
  38. data/spec/dummy/config/application.rb +47 -0
  39. data/spec/dummy/config/boot.rb +10 -0
  40. data/spec/dummy/config/database.yml +16 -0
  41. data/spec/dummy/config/environment.rb +5 -0
  42. data/spec/dummy/config/environments/development.rb +26 -0
  43. data/spec/dummy/config/environments/production.rb +49 -0
  44. data/spec/dummy/config/environments/test.rb +35 -0
  45. data/spec/dummy/config/initializers/apartment.rb +4 -0
  46. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  47. data/spec/dummy/config/initializers/inflections.rb +10 -0
  48. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  49. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  50. data/spec/dummy/config/initializers/session_store.rb +8 -0
  51. data/spec/dummy/config/locales/en.yml +5 -0
  52. data/spec/dummy/config/routes.rb +3 -0
  53. data/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb +37 -0
  54. data/spec/dummy/db/migrate/20111202022214_create_table_books.rb +13 -0
  55. data/spec/dummy/db/schema.rb +48 -0
  56. data/spec/dummy/db/seeds.rb +8 -0
  57. data/spec/dummy/db/test.sqlite3 +0 -0
  58. data/spec/dummy/lib/fake_dj_class.rb +6 -0
  59. data/spec/dummy/public/404.html +26 -0
  60. data/spec/dummy/public/422.html +26 -0
  61. data/spec/dummy/public/500.html +26 -0
  62. data/spec/dummy/public/favicon.ico +0 -0
  63. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  64. data/spec/dummy/script/rails +6 -0
  65. data/spec/integration/apartment_rake_integration_spec.rb +74 -0
  66. data/spec/integration/database_integration_spec.rb +200 -0
  67. data/spec/integration/delayed_job_integration_spec.rb +100 -0
  68. data/spec/integration/middleware/subdomain_elevator_spec.rb +63 -0
  69. data/spec/spec_helper.rb +31 -0
  70. data/spec/support/apartment_helpers.rb +32 -0
  71. data/spec/support/capybara_sessions.rb +15 -0
  72. data/spec/support/config.rb +11 -0
  73. data/spec/tasks/apartment_rake_spec.rb +118 -0
  74. data/spec/unit/config_spec.rb +78 -0
  75. data/spec/unit/middleware/subdomain_elevator_spec.rb +20 -0
  76. data/spec/unit/migrator_spec.rb +87 -0
  77. data/spec/unit/reloader_spec.rb +22 -0
  78. metadata +144 -0
@@ -0,0 +1,20 @@
1
+ module ActiveRecord
2
+ class Base
3
+
4
+ # Overriding Delayed Job's monkey_patch of ActiveRecord so that it works with Apartment
5
+ yaml_as "tag:ruby.yaml.org,2002:ActiveRecord"
6
+
7
+ def self.yaml_new(klass, tag, val)
8
+ Apartment::Database.process(val['database']) do
9
+ klass.find(val['attributes']['id'])
10
+ end
11
+ rescue ActiveRecord::RecordNotFound
12
+ raise Delayed::DeserializationError
13
+ end
14
+
15
+ def to_yaml_properties
16
+ ['@attributes', '@database'] # add in database attribute for serialization
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require 'delayed_job'
2
+ require 'apartment/delayed_job/active_record' # ensure that our AR hooks are loaded when queueing
3
+
4
+ module Apartment
5
+ module Delayed
6
+ module Job
7
+
8
+ # Will enqueue a job ensuring that it happens within the main 'public' database
9
+ #
10
+ # Note that this should not longer be required for versions >= 0.11.0 when using postgresql schemas
11
+ #
12
+ def self.enqueue(payload_object, options = {})
13
+ Apartment::Database.process do
14
+ ::Delayed::Job.enqueue(payload_object, options)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ require 'apartment/delayed_job/enqueue'
2
+
3
+ module Apartment
4
+ module Delayed
5
+ module Job
6
+
7
+ # Before and after hooks for performing Delayed Jobs within a particular apartment database
8
+ # Include these in your delayed jobs models and make sure provide a @database attr that will be serialized by DJ
9
+ # Note also that any models that are being serialized need the Apartment::Delayed::Requirements module mixed in to it
10
+ module Hooks
11
+
12
+ attr_accessor :database
13
+
14
+ def before(job)
15
+ Apartment::Database.switch(job.payload_object.database) if job.payload_object.database
16
+ end
17
+
18
+ def after
19
+ Apartment::Database.reset
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ require 'apartment/delayed_job/enqueue'
2
+
3
+ module Apartment
4
+ module Delayed
5
+
6
+ # Mix this module into any ActiveRecord model that gets serialized by DJ
7
+ module Requirements
8
+ attr_accessor :database
9
+
10
+ def self.included(klass)
11
+ klass.after_find :set_database # set db when records are pulled so they deserialize properly
12
+ klass.before_save :set_database # set db before records are saved so that they also get deserialized properly
13
+ end
14
+
15
+ private
16
+
17
+ def set_database
18
+ @database = Apartment::Database.current_database
19
+ end
20
+
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ module Apartment
2
+ module Elevators
3
+ # Provides a rack based db switching solution based on subdomains
4
+ # Assumes that database name should match subdomain
5
+ class Subdomain
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ request = ActionDispatch::Request.new(env)
13
+
14
+ database = subdomain(request)
15
+
16
+ Apartment::Database.switch database if database
17
+
18
+ @app.call(env)
19
+ end
20
+
21
+ def subdomain(request)
22
+ request.subdomain.present? && request.subdomain || nil
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Apartment
2
+
3
+ module Migrator
4
+
5
+ extend self
6
+
7
+ # Migrate to latest
8
+ def migrate(database)
9
+ Database.process(database){ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path) }
10
+ end
11
+
12
+ # Migrate up/down to a specific version
13
+ def run(direction, database, version)
14
+ Database.process(database){ ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_path, version) }
15
+ end
16
+
17
+ # rollback latest migration `step` number of times
18
+ def rollback(database, step = 1)
19
+ Database.process(database){ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_path, step) }
20
+ end
21
+ end
22
+
23
+ end
@@ -0,0 +1,54 @@
1
+ require 'rails'
2
+
3
+ module Apartment
4
+ class Railtie < Rails::Railtie
5
+
6
+ #
7
+ # Set up our default config options
8
+ # Do this before the app initializers run so we don't override custom settings
9
+ #
10
+ config.before_initialize do
11
+ Apartment.configure do |config|
12
+ config.excluded_models = []
13
+ config.use_postgres_schemas = true
14
+ config.database_names = []
15
+ config.seed_after_create = false
16
+ config.prepend_environment = true
17
+ end
18
+ end
19
+
20
+ # Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized
21
+ # Note that this doens't entirely work as expected in Development, because this is called before classes are reloaded
22
+ # See the above middleware/console declarations below to help with this. Hope to fix that soon.
23
+ #
24
+ config.to_prepare do
25
+ Apartment::Database.init
26
+ end
27
+
28
+ #
29
+ # Ensure rake tasks are loaded
30
+ #
31
+ rake_tasks do
32
+ load 'tasks/apartment.rake'
33
+ end
34
+
35
+ #
36
+ # The following initializers are a workaround to the fact that I can't properly hook into the rails reloader
37
+ # Note this is technically valid for any environment where cache_classes is false, for us, it's just development
38
+ #
39
+ if Rails.env.development?
40
+
41
+ # Apartment::Reloader is middleware to initialize things properly on each request to dev
42
+ initializer 'apartment.init' do |app|
43
+ app.config.middleware.use "Apartment::Reloader"
44
+ end
45
+
46
+ # Overrides reload! to also call Apartment::Database.init as well so that the reloaded classes have the proper table_names
47
+ console do
48
+ require 'apartment/console'
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ module Apartment
2
+
3
+ class Reloader
4
+
5
+ # Middleware used in development to init Apartment for each request
6
+ # Necessary due to code reload (annoying). When models are reloaded, they no longer have the proper table_name
7
+ # That is prepended with the schema (if using postgresql schemas)
8
+ # I couldn't figure out how to properly hook into the Rails reload process *after* files are reloaded
9
+ # so I've used this in the meantime.
10
+ #
11
+ # Also see apartment/console for the re-definition of reload! that re-init's Apartment
12
+ #
13
+ def initialize(app)
14
+ @app = app
15
+ end
16
+
17
+ def call(env)
18
+ Database.init
19
+ @app.call(env)
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,3 @@
1
+ module Apartment
2
+ VERSION = "0.14.1"
3
+ end
@@ -0,0 +1,70 @@
1
+ apartment_namespace = namespace :apartment do
2
+
3
+ desc "Migrate all multi-tenant databases"
4
+ task :migrate => 'db:migrate' do
5
+
6
+ Apartment.database_names.each do |db|
7
+ puts("Migrating #{db} database")
8
+ Apartment::Migrator.migrate db
9
+ end
10
+ end
11
+
12
+ desc "Seed all multi-tenant databases"
13
+ task :seed => 'db:seed' do
14
+
15
+ Apartment.database_names.each do |db|
16
+ puts("Seeding #{db} database")
17
+ Apartment::Database.process(db) do
18
+ Apartment::Database.seed
19
+ end
20
+ end
21
+ end
22
+
23
+ desc "Rolls the schema back to the previous version (specify steps w/ STEP=n) across all multi-tenant dbs."
24
+ task :rollback => 'db:rollback' do
25
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
26
+
27
+ Apartment.database_names.each do |db|
28
+ puts("Rolling back #{db} database")
29
+ Apartment::Migrator.rollback db, step
30
+ end
31
+ end
32
+
33
+ namespace :migrate do
34
+
35
+ desc 'Runs the "up" for a given migration VERSION across all multi-tenant dbs.'
36
+ task :up => 'db:migrate:up' do
37
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
38
+ raise 'VERSION is required' unless version
39
+
40
+ Apartment.database_names.each do |db|
41
+ puts("Migrating #{db} database up")
42
+ Apartment::Migrator.run :up, db, version
43
+ end
44
+ end
45
+
46
+ desc 'Runs the "down" for a given migration VERSION across all multi-tenant dbs.'
47
+ task :down => 'db:migrate:down' do
48
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
49
+ raise 'VERSION is required' unless version
50
+
51
+ Apartment.database_names.each do |db|
52
+ puts("Migrating #{db} database down")
53
+ Apartment::Migrator.run :down, db, version
54
+ end
55
+ end
56
+
57
+ desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
58
+ task :redo => 'db:migrate:redo' do
59
+ if ENV['VERSION']
60
+ apartment_namespace['migrate:down'].invoke
61
+ apartment_namespace['migrate:up'].invoke
62
+ else
63
+ apartment_namespace['rollback'].invoke
64
+ apartment_namespace['migrate'].invoke
65
+ end
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'apartment/adapters/mysql_adapter' # specific adapters get dynamically loaded based on adapter name, so we must manually require here
3
+
4
+ describe Apartment::Adapters::MysqlAdapter do
5
+
6
+ before do
7
+ ActiveRecord::Base.establish_connection Apartment::Test.config['connections']['mysql']
8
+ @mysql = Apartment::Database.mysql_adapter Apartment::Test.config['connections']['mysql'].symbolize_keys
9
+ end
10
+
11
+ after do
12
+ ActiveRecord::Base.clear_all_connections!
13
+ end
14
+
15
+ context "using databases" do
16
+
17
+ let(:database1){ 'first_database' }
18
+
19
+ before do
20
+ @mysql.create(database1)
21
+ end
22
+
23
+ after do
24
+ ActiveRecord::Base.connection.drop_database(@mysql.environmentify(database1))
25
+ end
26
+
27
+ describe "#create" do
28
+ it "should create the new database" do
29
+ ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect{|row| row[0]}.should include(@mysql.environmentify(database1))
30
+ end
31
+ end
32
+
33
+ end
34
+
35
+
36
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+ require 'apartment/adapters/postgresql_adapter' # specific adapters get dynamically loaded based on adapter name, so we must manually require here
3
+
4
+ describe Apartment::Adapters::PostgresqlAdapter do
5
+
6
+ before do
7
+ ActiveRecord::Base.establish_connection Apartment::Test.config['connections']['postgresql']
8
+ @schema_search_path = ActiveRecord::Base.connection.schema_search_path
9
+ end
10
+
11
+ after do
12
+ ActiveRecord::Base.clear_all_connections!
13
+ end
14
+
15
+ context "using schemas" do
16
+
17
+ let(:schema){ 'first_db_schema' }
18
+ let(:schema2){ 'another_db_schema' }
19
+ let(:database_names){ ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect{|row| row['nspname']} }
20
+
21
+ subject{ Apartment::Database.postgresql_adapter Apartment::Test.config['connections']['postgresql'].symbolize_keys }
22
+
23
+ before do
24
+ Apartment.use_postgres_schemas = true
25
+ subject.create(schema)
26
+ subject.create(schema2)
27
+ end
28
+
29
+ after do
30
+ # sometimes we manually drop these schemas in testing, dont' care if we can't drop hence rescue
31
+ subject.drop(schema) rescue true
32
+ subject.drop(schema2) rescue true
33
+ end
34
+
35
+ describe "#create" do
36
+
37
+ it "should create the new schema" do
38
+ database_names.should include(schema)
39
+ end
40
+
41
+ it "should load schema.rb to new schema" do
42
+ ActiveRecord::Base.connection.schema_search_path = schema
43
+ ActiveRecord::Base.connection.tables.should include('companies')
44
+ end
45
+
46
+ it "should reset connection when finished" do
47
+ ActiveRecord::Base.connection.schema_search_path.should_not == schema
48
+ end
49
+
50
+ it "should yield to block if passed" do
51
+ subject.drop(schema2) # so we don't get errors on creation
52
+
53
+ @count = 0 # set our variable so its visible in and outside of blocks
54
+
55
+ subject.create(schema2) do
56
+ @count = User.count
57
+ ActiveRecord::Base.connection.schema_search_path.should == schema2
58
+ User.create
59
+ end
60
+
61
+ subject.process(schema2){ User.count.should == @count + 1 }
62
+ end
63
+ end
64
+
65
+ describe "#drop" do
66
+
67
+ it "should delete the database" do
68
+ subject.switch schema # can't drop db we're currently connected to, ensure these are different
69
+ subject.drop schema2
70
+
71
+ database_names.should_not include(schema2)
72
+ end
73
+
74
+ it "should raise an error for unkown database" do
75
+ expect {
76
+ subject.drop "unknown_database"
77
+ }.to raise_error(Apartment::SchemaNotFound)
78
+ end
79
+ end
80
+
81
+
82
+ describe "#process" do
83
+ it "should connect" do
84
+ subject.process(schema) do
85
+ ActiveRecord::Base.connection.schema_search_path.should == schema
86
+ end
87
+ end
88
+
89
+ it "should reset" do
90
+ subject.process(schema)
91
+ ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
92
+ end
93
+
94
+ # We're often finding when using Apartment in tests, the `current_database` (ie the previously attached to schema)
95
+ # gets dropped, but process will try to return to that schema in a test. We should just reset if it doesnt exist
96
+ it "should not throw exception if current_database (schema) is no longer accessible" do
97
+ subject.switch(schema2)
98
+
99
+ expect {
100
+ subject.process(schema){ subject.drop(schema2) }
101
+ }.to_not raise_error(Apartment::SchemaNotFound)
102
+ end
103
+ end
104
+
105
+ describe "#reset" do
106
+ it "should reset connection" do
107
+ subject.switch(schema)
108
+ subject.reset
109
+ ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
110
+ end
111
+ end
112
+
113
+ describe "#switch" do
114
+ it "should connect to new schema" do
115
+ subject.switch(schema)
116
+ ActiveRecord::Base.connection.schema_search_path.should == schema
117
+ end
118
+
119
+ it "should reset connection if database is nil" do
120
+ subject.switch
121
+ ActiveRecord::Base.connection.schema_search_path.should == @schema_search_path
122
+ end
123
+ end
124
+
125
+ describe "#current_database" do
126
+ it "should return the current schema name" do
127
+ subject.switch(schema)
128
+ subject.current_database.should == schema
129
+ end
130
+ end
131
+
132
+ end
133
+
134
+ context "using databases" do
135
+ # TODO
136
+ end
137
+ end