apartment 0.14.4 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
data/HISTORY.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.15.0
2
+ * March 18, 2012
3
+
4
+ - Remove Rails dependency, Apartment can now be used with any Rack based framework using ActiveRecord
5
+
1
6
  # 0.14.4
2
7
  * March 8, 2012
3
8
 
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Apartment
2
- *Multitenancy for Rails 3*
2
+ *Multitenancy for Rails 3 and ActiveRecord*
3
3
 
4
4
  Apartment provides tools to help you deal with multiple databases in your Rails
5
5
  application. If you need to have certain data sequestered based on account or company,
@@ -19,7 +19,6 @@ on a per-user basis, look under "Usage - Switching databases per request", below
19
19
 
20
20
  > NOTE: If using [postgresl schemas](http://www.postgresql.org/docs/9.0/static/ddl-schemas.html) you must use:
21
21
  >
22
- > * for Rails 3.0.x: _Rails ~> 3.0.10_, it contains a [patch](https://github.com/rails/rails/pull/1607) that has better postgresql schema support
23
22
  > * for Rails 3.1.x: _Rails ~> 3.1.2_, it contains a [patch](https://github.com/rails/rails/pull/3232) that makes prepared statements work with multiple schemas
24
23
 
25
24
  ## Usage
@@ -157,8 +156,4 @@ In order to make ActiveRecord models play nice with DJ and Apartment, include `A
157
156
  * The Local setup for development assumes that a root user with no password exists for both mysql and postgresl
158
157
  * Rake tasks (see the Rakefile) will help you setup your dbs necessary to run tests
159
158
  * Please issue pull requests to the `development` branch. All development happens here, master is used for releases
160
- * Ensure that your code is accompanied with tests. No code will be merged without tests
161
-
162
- ## TODO
163
-
164
- * Shared examples for testing to ensure consistency across all adapters
159
+ * Ensure that your code is accompanied with tests. No code will be merged without tests
@@ -17,16 +17,18 @@ Gem::Specification.new do |s|
17
17
  s.licenses = ["MIT"]
18
18
  s.require_paths = ["lib"]
19
19
  s.rubygems_version = %q{1.3.7}
20
-
21
- s.add_dependency 'rails', '>= 3.1.2'
20
+
21
+ s.add_dependency 'activerecord', '>= 3.1.2' # must be >= 3.1.2 due to bug in prepared_statements
22
+ s.add_dependency 'rack', '~> 1.4.0'
23
+
24
+ s.add_development_dependency 'rails', '>= 3.1.2'
22
25
  s.add_development_dependency 'rake', '~> 0.9.2'
23
26
  s.add_development_dependency 'sqlite3'
24
27
  s.add_development_dependency 'rspec', '~> 2.8.0'
25
- s.add_development_dependency 'rspec-rails', '~> 2.8.0'
26
- s.add_development_dependency 'capybara', '1.0.0'
27
- s.add_development_dependency 'pg', '~> 0.11.0'
28
- s.add_development_dependency 'mysql2', '~> 0.3.7'
29
- s.add_development_dependency "silent-postgres", "~> 0.1.1"
30
- s.add_development_dependency 'delayed_job', '~> 3.0.1'
28
+ s.add_development_dependency 'rspec-rails', '~> 2.8.1'
29
+ s.add_development_dependency 'capybara', '~> 1.0.0'
30
+ s.add_development_dependency 'pg', '>= 0.11.0'
31
+ s.add_development_dependency 'mysql2', '~> 0.3.10'
32
+ s.add_development_dependency 'delayed_job', '~> 3.0'
31
33
  s.add_development_dependency 'delayed_job_active_record'
32
34
  end
@@ -16,7 +16,7 @@ module Apartment
16
16
  @database_names.respond_to?(:call) ? @database_names.call : @database_names
17
17
  end
18
18
 
19
- # Default to none
19
+ # Default to empty array
20
20
  def excluded_models
21
21
  @excluded_models || []
22
22
  end
@@ -1,24 +1,24 @@
1
1
  require 'active_record'
2
2
 
3
3
  module Apartment
4
-
4
+
5
5
  module Adapters
6
-
6
+
7
7
  class AbstractAdapter
8
-
8
+
9
9
  # @constructor
10
10
  # @param {Hash} config Database config
11
11
  # @param {Hash} defaults Some default options
12
- #
12
+ #
13
13
  def initialize(config, defaults = {})
14
14
  @config = config
15
15
  @defaults = defaults
16
16
  end
17
-
17
+
18
18
  # Create a new database, import schema, seed if appropriate
19
- #
19
+ #
20
20
  # @param {String} database Database name
21
- #
21
+ #
22
22
  def create(database)
23
23
  create_database(database)
24
24
 
@@ -27,44 +27,35 @@ module Apartment
27
27
 
28
28
  # Seed data if appropriate
29
29
  seed_data if Apartment.seed_after_create
30
-
30
+
31
31
  yield if block_given?
32
32
  end
33
33
  end
34
-
34
+
35
35
  # Get the current database name
36
- #
36
+ #
37
37
  # @return {String} current database name
38
- #
38
+ #
39
39
  def current_database
40
40
  ActiveRecord::Base.connection.current_database
41
41
  end
42
-
42
+
43
43
  # Drop the database
44
- #
44
+ #
45
45
  # @param {String} database Database name
46
- #
46
+ #
47
47
  def drop(database)
48
48
  # ActiveRecord::Base.connection.drop_database note that drop_database will not throw an exception, so manually execute
49
49
  ActiveRecord::Base.connection.execute("DROP DATABASE #{environmentify(database)}" )
50
-
51
- rescue ActiveRecord::StatementInvalid => e
50
+
51
+ rescue ActiveRecord::StatementInvalid
52
52
  raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found"
53
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
54
 
64
55
  # Connect to db, do your biz, switch back to previous db
65
- #
56
+ #
66
57
  # @param {String?} database Database or schema to connect to
67
- #
58
+ #
68
59
  def process(database = nil)
69
60
  current_db = current_database
70
61
  switch(database)
@@ -75,7 +66,7 @@ module Apartment
75
66
  end
76
67
 
77
68
  # Establish a new connection for each specific excluded model
78
- #
69
+ #
79
70
  def process_excluded_models
80
71
  # All other models will shared a connection (at ActiveRecord::Base) and we can modify at will
81
72
  Apartment.excluded_models.each do |excluded_model|
@@ -85,21 +76,21 @@ module Apartment
85
76
  warn "[Deprecation Warning] Passing class references to excluded models is now deprecated, please use a string instead"
86
77
  excluded_model = excluded_model.name
87
78
  end
88
-
79
+
89
80
  excluded_model.constantize.establish_connection @config
90
81
  end
91
82
  end
92
-
83
+
93
84
  # Reset the database connection to the default
94
- #
85
+ #
95
86
  def reset
96
87
  ActiveRecord::Base.establish_connection @config
97
88
  end
98
-
89
+
99
90
  # Switch to new connection (or schema if appopriate)
100
- #
91
+ #
101
92
  # @param {String} database Database name
102
- #
93
+ #
103
94
  def switch(database = nil)
104
95
  # Just connect to default db and return
105
96
  return reset if database.nil?
@@ -108,54 +99,63 @@ module Apartment
108
99
  end
109
100
 
110
101
  # Load the rails seed file into the db
111
- #
102
+ #
112
103
  def seed_data
113
104
  silence_stream(STDOUT){ load_or_abort("#{Rails.root}/db/seeds.rb") } # Don't log the output of seeding the db
114
105
  end
115
106
  alias_method :seed, :seed_data
116
-
107
+
117
108
  protected
118
-
109
+
119
110
  # Create the database
120
- #
111
+ #
121
112
  # @param {String} database Database name
122
- #
113
+ #
123
114
  def create_database(database)
124
115
  ActiveRecord::Base.connection.create_database( environmentify(database) )
125
116
 
126
- rescue ActiveRecord::StatementInvalid => e
117
+ rescue ActiveRecord::StatementInvalid
127
118
  raise DatabaseExists, "The database #{environmentify(database)} already exists."
128
119
  end
129
-
120
+
130
121
  # Connect to new database
131
- #
122
+ #
132
123
  # @param {String} database Database name
133
- #
124
+ #
134
125
  def connect_to_new(database)
135
126
  ActiveRecord::Base.establish_connection multi_tenantify(database)
136
127
  ActiveRecord::Base.connection.active? # call active? to manually check if this connection is valid
137
128
 
138
- rescue ActiveRecord::StatementInvalid => e
129
+ rescue ActiveRecord::StatementInvalid
139
130
  raise DatabaseNotFound, "The database #{environmentify(database)} cannot be found."
140
131
  end
141
-
132
+
133
+ # Prepend the environment if configured and the environment isn't already there
134
+ #
135
+ # @param {String} database Database name
136
+ # @return {String} database name with Rails environment *optionally* prepended
137
+ #
138
+ def environmentify(database)
139
+ Apartment.prepend_environment && !database.include?(Rails.env) ? "#{Rails.env}_#{database}" : database
140
+ end
141
+
142
142
  # Import the database schema
143
- #
143
+ #
144
144
  def import_database_schema
145
145
  ActiveRecord::Schema.verbose = false # do not log schema load output.
146
146
  load_or_abort("#{Rails.root}/db/schema.rb")
147
147
  end
148
-
148
+
149
149
  # Return a new config that is multi-tenanted
150
- #
150
+ #
151
151
  def multi_tenantify(database)
152
152
  @config.clone.tap do |config|
153
153
  config[:database] = environmentify(database)
154
154
  end
155
155
  end
156
-
156
+
157
157
  # Load a file or abort if it doesn't exists
158
- #
158
+ #
159
159
  def load_or_abort(file)
160
160
  if File.exists?(file)
161
161
  load(file)
@@ -163,14 +163,7 @@ module Apartment
163
163
  abort %{#{file} doesn't exist yet}
164
164
  end
165
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
-
166
+
174
167
  end
175
168
  end
176
169
  end
@@ -2,7 +2,7 @@ module Apartment
2
2
 
3
3
  module Database
4
4
 
5
- def self.mysql_adapter(config)
5
+ def self.mysql2_adapter(config)
6
6
  Adapters::MysqlAdapter.new config
7
7
  end
8
8
  end
@@ -11,8 +11,19 @@ module Apartment
11
11
 
12
12
  class MysqlAdapter < AbstractAdapter
13
13
 
14
+ protected
15
+
16
+ # Connect to new database
17
+ # Abstract adapter will catch generic ActiveRecord error
18
+ # Catch specific adapter errors here
19
+ #
20
+ # @param {String} database Database name
21
+ #
22
+ def connect_to_new(database)
23
+ super
24
+ rescue Mysql2::Error
25
+ raise DatabaseNotFound, "Cannot find database #{environmentify(database)}"
26
+ end
14
27
  end
15
-
16
28
  end
17
-
18
29
  end
@@ -7,7 +7,6 @@ module Apartment
7
7
  Adapters::PostgresqlSchemaAdapter.new(config, :schema_search_path => ActiveRecord::Base.connection.schema_search_path) :
8
8
  Adapters::PostgresqlAdapter.new(config)
9
9
  end
10
-
11
10
  end
12
11
 
13
12
  module Adapters
@@ -47,7 +46,7 @@ module Apartment
47
46
  # @param {String} database Database (schema) to drop
48
47
  #
49
48
  def drop(database)
50
- ActiveRecord::Base.connection.execute("DROP SCHEMA #{database} CASCADE")
49
+ ActiveRecord::Base.connection.execute("DROP SCHEMA \"#{database}\" CASCADE")
51
50
 
52
51
  rescue ActiveRecord::StatementInvalid
53
52
  raise SchemaNotFound, "The schema #{database.inspect} cannot be found."
@@ -102,13 +101,12 @@ module Apartment
102
101
  # Create the new schema
103
102
  #
104
103
  def create_database(database)
105
- ActiveRecord::Base.connection.execute("CREATE SCHEMA #{database}")
104
+ ActiveRecord::Base.connection.execute("CREATE SCHEMA \"#{database}\"")
106
105
 
107
106
  rescue ActiveRecord::StatementInvalid
108
107
  raise SchemaExists, "The schema #{database} already exists."
109
108
  end
110
109
 
111
110
  end
112
-
113
111
  end
114
112
  end
@@ -13,7 +13,7 @@ module Apartment
13
13
  config.use_postgres_schemas = true
14
14
  config.database_names = []
15
15
  config.seed_after_create = false
16
- config.prepend_environment = true
16
+ config.prepend_environment = false
17
17
  end
18
18
  end
19
19
 
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "0.14.4"
2
+ VERSION = "0.15.0"
3
3
  end
@@ -1,36 +1,17 @@
1
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
2
+ require 'apartment/adapters/mysql_adapter'
3
3
 
4
4
  describe Apartment::Adapters::MysqlAdapter do
5
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
6
+ let(:config){ Apartment::Test.config['connections']['mysql'] }
7
+ subject{ Apartment::Database.mysql2_adapter config.symbolize_keys }
14
8
 
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
-
9
+ def database_names
10
+ ActiveRecord::Base.connection.execute("SELECT schema_name FROM information_schema.schemata").collect{|row| row[0]}
33
11
  end
34
12
 
13
+ let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
35
14
 
15
+ it_should_behave_like "a generic apartment adapter"
16
+ it_should_behave_like "a db based apartment adapter"
36
17
  end
@@ -1,137 +1,38 @@
1
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
2
+ require 'apartment/adapters/postgresql_adapter'
3
3
 
4
4
  describe Apartment::Adapters::PostgresqlAdapter do
5
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
6
+ let(:config){ Apartment::Test.config['connections']['postgresql'] }
7
+ subject{ Apartment::Database.postgresql_adapter config.symbolize_keys }
14
8
 
15
9
  context "using schemas" do
16
10
 
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)
11
+ before{ Apartment.use_postgres_schemas = true }
98
12
 
99
- expect {
100
- subject.process(schema){ subject.drop(schema2) }
101
- }.to_not raise_error(Apartment::SchemaNotFound)
102
- end
13
+ # Not sure why, but somehow using let(:database_names) memoizes for the whole example group, not just each test
14
+ def database_names
15
+ ActiveRecord::Base.connection.execute("SELECT nspname FROM pg_namespace;").collect{|row| row['nspname']}
103
16
  end
17
+
18
+ let(:default_database){ subject.process{ ActiveRecord::Base.connection.schema_search_path } }
104
19
 
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
20
+ it_should_behave_like "a generic apartment adapter"
21
+ it_should_behave_like "a schema based apartment adapter"
22
+ end
23
+
24
+ context "using databases" do
118
25
 
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
26
+ before{ Apartment.use_postgres_schemas = false }
124
27
 
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
28
+ # Not sure why, but somehow using let(:database_names) memoizes for the whole example group, not just each test
29
+ def database_names
30
+ connection.execute("select datname from pg_database;").collect{|row| row['datname']}
130
31
  end
32
+
33
+ let(:default_database){ subject.process{ ActiveRecord::Base.connection.current_database } }
131
34
 
132
- end
133
-
134
- context "using databases" do
135
- # TODO
35
+ it_should_behave_like "a generic apartment adapter"
36
+ it_should_behave_like "a db based apartment adapter"
136
37
  end
137
38
  end