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,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