apartment 0.14.4 → 0.15.0

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