dr-apartment 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
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,176 @@
1
+ require 'active_record'
2
+
3
+ module Apartment
4
+
5
+ module Adapters
6
+
7
+ class AbstractAdapter
8
+
9
+ # @constructor
10
+ # @param {Hash} config Database config
11
+ # @param {Hash} defaults Some default options
12
+ #
13
+ def initialize(config, defaults = {})
14
+ @config = config
15
+ @defaults = defaults
16
+ end
17
+
18
+ # Create a new database, import schema, seed if appropriate
19
+ #
20
+ # @param {String} database Database name
21
+ #
22
+ def create(database)
23
+ create_database(database)
24
+
25
+ process(database) do
26
+ import_database_schema
27
+
28
+ # Seed data if appropriate
29
+ seed_data if Apartment.seed_after_create
30
+
31
+ yield if block_given?
32
+ end
33
+ end
34
+
35
+ # Get the current database name
36
+ #
37
+ # @return {String} current database name
38
+ #
39
+ def current_database
40
+ ActiveRecord::Base.connection.current_database
41
+ end
42
+
43
+ # Drop the database
44
+ #
45
+ # @param {String} database Database name
46
+ #
47
+ def drop(database)
48
+ # ActiveRecord::Base.connection.drop_database note that drop_database will not throw an exception, so manually execute
49
+ ActiveRecord::Base.connection.execute("DROP DATABASE #{environmentify(database)}" )
50
+
51
+ rescue ActiveRecord::StatementInvalid => e
52
+ raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found"
53
+ end
54
+
55
+ # Prepend the environment if configured and the environment isn't already there
56
+ #
57
+ # @param {String} database Database name
58
+ # @return {String} database name with Rails environment *optionally* prepended
59
+ #
60
+ def environmentify(database)
61
+ Apartment.prepend_environment && !database.include?(Rails.env) ? "#{Rails.env}_#{database}" : database
62
+ end
63
+
64
+ # Connect to db, do your biz, switch back to previous db
65
+ #
66
+ # @param {String?} database Database or schema to connect to
67
+ #
68
+ def process(database = nil)
69
+ current_db = current_database
70
+ switch(database)
71
+ yield if block_given?
72
+
73
+ ensure
74
+ switch(current_db) rescue reset
75
+ end
76
+
77
+ # Establish a new connection for each specific excluded model
78
+ #
79
+ def process_excluded_models
80
+ # All other models will shared a connection (at ActiveRecord::Base) and we can modify at will
81
+ Apartment.excluded_models.each do |excluded_model|
82
+ # Note that due to rails reloading, we now take string references to classes rather than
83
+ # actual object references. This way when we contantize, we always get the proper class reference
84
+ if excluded_model.is_a? Class
85
+ warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
86
+ excluded_model = excluded_model.name
87
+ end
88
+
89
+ excluded_model.constantize.establish_connection @config
90
+ end
91
+ end
92
+
93
+ # Reset the database connection to the default
94
+ #
95
+ def reset
96
+ ActiveRecord::Base.establish_connection @config
97
+ end
98
+
99
+ # Switch to new connection (or schema if appopriate)
100
+ #
101
+ # @param {String} database Database name
102
+ #
103
+ def switch(database = nil)
104
+ # Just connect to default db and return
105
+ return reset if database.nil?
106
+
107
+ connect_to_new(database)
108
+ end
109
+
110
+ # Load the rails seed file into the db
111
+ #
112
+ def seed_data
113
+ silence_stream(STDOUT){ load_or_abort("#{Rails.root}/db/seeds.rb") } # Don't log the output of seeding the db
114
+ end
115
+ alias_method :seed, :seed_data
116
+
117
+ protected
118
+
119
+ # Create the database
120
+ #
121
+ # @param {String} database Database name
122
+ #
123
+ def create_database(database)
124
+ ActiveRecord::Base.connection.create_database( environmentify(database) )
125
+
126
+ rescue ActiveRecord::StatementInvalid => e
127
+ raise DatabaseExists, "The database #{environmentify(database)} already exists."
128
+ end
129
+
130
+ # Connect to new database
131
+ #
132
+ # @param {String} database Database name
133
+ #
134
+ def connect_to_new(database)
135
+ ActiveRecord::Base.establish_connection multi_tenantify(database)
136
+ ActiveRecord::Base.connection.active? # call active? to manually check if this connection is valid
137
+
138
+ rescue ActiveRecord::StatementInvalid => e
139
+ raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found."
140
+ end
141
+
142
+ # Import the database schema
143
+ #
144
+ def import_database_schema
145
+ ActiveRecord::Schema.verbose = false # do not log schema load output.
146
+ load_or_abort("#{Rails.root}/db/schema.rb")
147
+ end
148
+
149
+ # Return a new config that is multi-tenanted
150
+ #
151
+ def multi_tenantify(database)
152
+ @config.clone.tap do |config|
153
+ config[:database] = environmentify(database)
154
+ end
155
+ end
156
+
157
+ # Load a file or abort if it doesn't exists
158
+ #
159
+ def load_or_abort(file)
160
+ if File.exists?(file)
161
+ load(file)
162
+ else
163
+ abort %{#{file} doesn't exist yet}
164
+ end
165
+ end
166
+
167
+ # Remove all non-alphanumeric characters
168
+ #
169
+ def sanitize(database)
170
+ warn "[Deprecation Warning] Sanitize is no longer used, client should ensure proper database names"
171
+ database.gsub(/[\W]/,'')
172
+ end
173
+
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,115 @@
1
+ module Apartment
2
+
3
+ module Database
4
+
5
+ def self.jdbcpostgresql_adapter(config)
6
+ Apartment.use_postgres_schemas ?
7
+ Adapters::PostgresqlSchemaAdapter.new(config, :schema_search_path => ActiveRecord::Base.connection.schema_search_path) :
8
+ Adapters::PostgresqlAdapter.new(config)
9
+ end
10
+
11
+ end
12
+
13
+ module Adapters
14
+
15
+ # Default adapter when not using Postgresql Schemas
16
+ class PostgresqlAdapter < AbstractAdapter
17
+
18
+ protected
19
+
20
+ # Connect to new database
21
+ # Abstract adapter will catch generic ActiveRecord error
22
+ # Catch specific adapter errors here
23
+ #
24
+ # @param {String} database Database name
25
+ #
26
+ def connect_to_new(database)
27
+ super
28
+ rescue PGError
29
+ raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
30
+ end
31
+
32
+ end
33
+
34
+ # Separate Adapter for Postgresql when using schemas
35
+ class PostgresqlSchemaAdapter < AbstractAdapter
36
+ puts ActiveRecord::Base.connection.class
37
+
38
+ # Get the current schema search path
39
+ #
40
+ # @return {String} current schema search path
41
+ #
42
+ def current_database
43
+ ActiveRecord::Base.connection.schema_search_path
44
+ end
45
+
46
+ # Drop the database schema
47
+ #
48
+ # @param {String} database Database (schema) to drop
49
+ #
50
+ def drop(database)
51
+ ActiveRecord::Base.connection.execute("DROP SCHEMA #{database} CASCADE")
52
+
53
+ rescue ActiveRecord::StatementInvalid
54
+ raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
55
+ end
56
+
57
+ # Reset search path to default search_path
58
+ # Set the table_name to always use the public namespace for excluded models
59
+ #
60
+ def process_excluded_models
61
+ Apartment.excluded_models.each do |excluded_model|
62
+ # Note that due to rails reloading, we now take string references to classes rather than
63
+ # actual object references. This way when we contantize, we always get the proper class reference
64
+ if excluded_model.is_a? Class
65
+ warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
66
+ excluded_model = excluded_model.name
67
+ end
68
+
69
+ excluded_model.constantize.tap do |klass|
70
+ # some models (such as delayed_job) seem to load and cache their column names before this,
71
+ # so would never get the public prefix, so reset first
72
+ klass.reset_column_information
73
+
74
+ # Ensure that if a schema *was* set, we override
75
+ table_name = klass.table_name.split('.', 2).last
76
+
77
+ # Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
78
+ klass.table_name = "public.#{table_name}"
79
+ end
80
+ end
81
+ end
82
+
83
+ # Reset schema search path to the default schema_search_path
84
+ #
85
+ # @return {String} default schema search path
86
+ #
87
+ def reset
88
+ ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path]
89
+ end
90
+
91
+ protected
92
+
93
+ # Set schema search path to new schema
94
+ #
95
+ def connect_to_new(database = nil)
96
+ return reset if database.nil?
97
+ ActiveRecord::Base.connection.schema_search_path = database
98
+
99
+ rescue ActiveRecord::StatementInvalid
100
+ raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
101
+ end
102
+
103
+ # Create the new schema
104
+ #
105
+ def create_database(database)
106
+ ActiveRecord::Base.connection.execute("CREATE SCHEMA #{database}")
107
+
108
+ rescue ActiveRecord::StatementInvalid
109
+ raise SchemaExists, "The schema #{database} already exists."
110
+ end
111
+
112
+ end
113
+
114
+ end
115
+ end
@@ -0,0 +1,18 @@
1
+ module Apartment
2
+
3
+ module Database
4
+
5
+ def self.mysql_adapter(config)
6
+ Adapters::MysqlAdapter.new config
7
+ end
8
+ end
9
+
10
+ module Adapters
11
+
12
+ class MysqlAdapter < AbstractAdapter
13
+
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,114 @@
1
+ module Apartment
2
+
3
+ module Database
4
+
5
+ def self.postgresql_adapter(config)
6
+ Apartment.use_postgres_schemas ?
7
+ Adapters::PostgresqlSchemaAdapter.new(config, :schema_search_path => ActiveRecord::Base.connection.schema_search_path) :
8
+ Adapters::PostgresqlAdapter.new(config)
9
+ end
10
+
11
+ end
12
+
13
+ module Adapters
14
+
15
+ # Default adapter when not using Postgresql Schemas
16
+ class PostgresqlAdapter < AbstractAdapter
17
+
18
+ protected
19
+
20
+ # Connect to new database
21
+ # Abstract adapter will catch generic ActiveRecord error
22
+ # Catch specific adapter errors here
23
+ #
24
+ # @param {String} database Database name
25
+ #
26
+ def connect_to_new(database)
27
+ super
28
+ rescue PGError
29
+ raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
30
+ end
31
+
32
+ end
33
+
34
+ # Separate Adapter for Postgresql when using schemas
35
+ class PostgresqlSchemaAdapter < AbstractAdapter
36
+
37
+ # Get the current schema search path
38
+ #
39
+ # @return {String} current schema search path
40
+ #
41
+ def current_database
42
+ ActiveRecord::Base.connection.schema_search_path
43
+ end
44
+
45
+ # Drop the database schema
46
+ #
47
+ # @param {String} database Database (schema) to drop
48
+ #
49
+ def drop(database)
50
+ ActiveRecord::Base.connection.execute("DROP SCHEMA #{database} CASCADE")
51
+
52
+ rescue ActiveRecord::StatementInvalid
53
+ raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
54
+ end
55
+
56
+ # Reset search path to default search_path
57
+ # Set the table_name to always use the public namespace for excluded models
58
+ #
59
+ def process_excluded_models
60
+ Apartment.excluded_models.each do |excluded_model|
61
+ # Note that due to rails reloading, we now take string references to classes rather than
62
+ # actual object references. This way when we contantize, we always get the proper class reference
63
+ if excluded_model.is_a? Class
64
+ warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
65
+ excluded_model = excluded_model.name
66
+ end
67
+
68
+ excluded_model.constantize.tap do |klass|
69
+ # some models (such as delayed_job) seem to load and cache their column names before this,
70
+ # so would never get the public prefix, so reset first
71
+ klass.reset_column_information
72
+
73
+ # Ensure that if a schema *was* set, we override
74
+ table_name = klass.table_name.split('.', 2).last
75
+
76
+ # Not sure why, but Delayed::Job somehow ignores table_name_prefix... so we'll just manually set table name instead
77
+ klass.table_name = "public.#{table_name}"
78
+ end
79
+ end
80
+ end
81
+
82
+ # Reset schema search path to the default schema_search_path
83
+ #
84
+ # @return {String} default schema search path
85
+ #
86
+ def reset
87
+ ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path]
88
+ end
89
+
90
+ protected
91
+
92
+ # Set schema search path to new schema
93
+ #
94
+ def connect_to_new(database = nil)
95
+ return reset if database.nil?
96
+ ActiveRecord::Base.connection.schema_search_path = database
97
+
98
+ rescue ActiveRecord::StatementInvalid
99
+ raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
100
+ end
101
+
102
+ # Create the new schema
103
+ #
104
+ def create_database(database)
105
+ ActiveRecord::Base.connection.execute("CREATE SCHEMA #{database}")
106
+
107
+ rescue ActiveRecord::StatementInvalid
108
+ raise SchemaExists, "The schema #{database} already exists."
109
+ end
110
+
111
+ end
112
+
113
+ end
114
+ end
@@ -0,0 +1,12 @@
1
+ # A workaraound to get `reload!` to also call Apartment::Database.init
2
+ # This is unfortunate, but I haven't figured out how to hook into the reload process *after* files are reloaded
3
+
4
+ # reloads the environment
5
+ def reload!(print=true)
6
+ puts "Reloading..." if print
7
+ # This triggers the to_prepare callbacks
8
+ ActionDispatch::Callbacks.new(Proc.new {}).call({})
9
+ # Manually init Apartment again once classes are reloaded
10
+ Apartment::Database.init
11
+ true
12
+ end
@@ -0,0 +1,57 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module Apartment
4
+
5
+ # The main entry point to Apartment functions
6
+ module Database
7
+
8
+ extend self
9
+
10
+ delegate :create, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch, :to => :adapter
11
+
12
+ # Initialize Apartment config options such as excluded_models
13
+ #
14
+ def init
15
+ process_excluded_models
16
+ end
17
+
18
+ # Fetch the proper multi-tenant adapter based on Rails config
19
+ #
20
+ # @return {subclass of Apartment::AbstractAdapter}
21
+ #
22
+ def adapter
23
+ @adapter ||= begin
24
+ adapter_method = "#{config[:adapter]}_adapter"
25
+
26
+ begin
27
+ require "apartment/adapters/#{adapter_method}"
28
+ rescue LoadError => e
29
+ raise "The adapter `#{config[:adapter]}` is not yet supported"
30
+ end
31
+
32
+ unless respond_to?(adapter_method)
33
+ raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
34
+ end
35
+
36
+ send(adapter_method, config)
37
+ end
38
+ end
39
+
40
+ # Reset config and adapter so they are regenerated
41
+ #
42
+ def reload!
43
+ @adapter = nil
44
+ @config = nil
45
+ end
46
+
47
+ private
48
+
49
+ # Fetch the rails database configuration
50
+ #
51
+ def config
52
+ @config ||= Rails.configuration.database_configuration[Rails.env].symbolize_keys
53
+ end
54
+
55
+ end
56
+
57
+ end