database_cleaner-active_record 1.99.0 → 2.0.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.
@@ -1,59 +1,22 @@
1
1
  require 'database_cleaner/active_record/base'
2
- require 'database_cleaner/generic/transaction'
3
2
 
4
- module DatabaseCleaner::ActiveRecord
5
- class Transaction
6
- include ::DatabaseCleaner::ActiveRecord::Base
7
- include ::DatabaseCleaner::Generic::Transaction
3
+ module DatabaseCleaner
4
+ module ActiveRecord
5
+ class Transaction < Base
6
+ def start
7
+ # Hack to make sure that the connection is properly set up before cleaning
8
+ connection_class.connection.transaction {}
8
9
 
9
- def start
10
- # Hack to make sure that the connection is properly setup for
11
- # the clean code.
12
- connection_class.connection.transaction{ }
13
-
14
- if connection_maintains_transaction_count?
15
- if connection_class.connection.respond_to?(:increment_open_transactions)
16
- connection_class.connection.increment_open_transactions
17
- else
18
- connection_class.__send__(:increment_open_transactions)
19
- end
20
- end
21
- if connection_class.connection.respond_to?(:begin_transaction)
22
- connection_class.connection.begin_transaction :joinable => false
23
- else
24
- connection_class.connection.begin_db_transaction
10
+ connection_class.connection.begin_transaction joinable: false
25
11
  end
26
- end
27
-
28
12
 
29
- def clean
30
- connection_class.connection_pool.connections.each do |connection|
31
- next unless connection.open_transactions > 0
32
13
 
33
- if connection.respond_to?(:rollback_transaction)
14
+ def clean
15
+ connection_class.connection_pool.connections.each do |connection|
16
+ next unless connection.open_transactions > 0
34
17
  connection.rollback_transaction
35
- else
36
- connection.rollback_db_transaction
37
- end
38
-
39
- # The below is for handling after_commit hooks.. see https://github.com/bmabey/database_cleaner/issues/99
40
- if connection.respond_to?(:rollback_transaction_records, true)
41
- connection.send(:rollback_transaction_records, true)
42
- end
43
-
44
- if connection_maintains_transaction_count?
45
- if connection.respond_to?(:decrement_open_transactions)
46
- connection.decrement_open_transactions
47
- else
48
- connection_class.__send__(:decrement_open_transactions)
49
- end
50
18
  end
51
19
  end
52
20
  end
53
-
54
- def connection_maintains_transaction_count?
55
- ActiveRecord::VERSION::MAJOR < 4
56
- end
57
-
58
21
  end
59
22
  end
@@ -1,288 +1,249 @@
1
+ require "delegate"
1
2
  require 'active_record/base'
2
3
  require 'database_cleaner/active_record/base'
3
- require 'active_record/connection_adapters/abstract_adapter'
4
- require 'database_cleaner/deprecation'
5
-
6
- #Load available connection adapters
7
- %w(
8
- abstract_mysql_adapter postgresql_adapter sqlite3_adapter mysql_adapter mysql2_adapter oracle_enhanced_adapter
9
- ).each do |known_adapter|
10
- begin
11
- require "active_record/connection_adapters/#{known_adapter}"
12
- rescue LoadError
13
- end
14
- end
15
-
16
- require "database_cleaner/generic/truncation"
17
- require 'database_cleaner/active_record/base'
18
4
 
19
5
  module DatabaseCleaner
20
- module ConnectionAdapters
21
-
22
- module AbstractAdapter
23
- # used to be called views but that can clash with gems like schema_plus
24
- # this gem is not meant to be exposing such an extra interface any way
25
- def database_cleaner_view_cache
26
- @views ||= select_values("select table_name from information_schema.views where table_schema = '#{current_database}'") rescue []
27
- end
28
-
29
- def database_cleaner_table_cache
30
- # the adapters don't do caching (#130) but we make the assumption that the list stays the same in tests
31
- @database_cleaner_tables ||= database_tables
32
- end
33
-
34
- def database_tables
35
- ::ActiveRecord::VERSION::MAJOR >= 5 ? data_sources : tables
36
- end
37
-
38
- def truncate_table(table_name)
39
- raise NotImplementedError
40
- end
41
-
42
- def truncate_tables(tables)
43
- tables.each do |table_name|
44
- self.truncate_table(table_name)
6
+ module ActiveRecord
7
+ class Truncation < Base
8
+ def initialize(opts={})
9
+ if !opts.empty? && !(opts.keys - [:only, :except, :pre_count, :cache_tables]).empty?
10
+ raise ArgumentError, "The only valid options are :only, :except, :pre_count, and :cache_tables. You specified #{opts.keys.join(',')}."
45
11
  end
46
- end
47
- end
48
12
 
49
- module AbstractMysqlAdapter
50
- def truncate_table(table_name)
51
- execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
52
- end
13
+ @only = Array(opts[:only]).dup
14
+ @except = Array(opts[:except]).dup
53
15
 
54
- def truncate_tables(tables)
55
- tables.each { |t| truncate_table(t) }
16
+ @pre_count = opts[:pre_count]
17
+ @cache_tables = opts.has_key?(:cache_tables) ? !!opts[:cache_tables] : true
56
18
  end
57
19
 
58
- def pre_count_truncate_tables(tables, options = {:reset_ids => true})
59
- filter = options[:reset_ids] ? method(:has_been_used?) : method(:has_rows?)
60
- truncate_tables(tables.select(&filter))
20
+ def clean
21
+ connection.disable_referential_integrity do
22
+ if pre_count? && connection.respond_to?(:pre_count_truncate_tables)
23
+ connection.pre_count_truncate_tables(tables_to_truncate(connection))
24
+ else
25
+ connection.truncate_tables(tables_to_truncate(connection))
26
+ end
27
+ end
61
28
  end
62
29
 
63
30
  private
64
31
 
65
- def row_count(table)
66
- # Patch for MysqlAdapter with ActiveRecord 3.2.7 later
67
- # select_value("SELECT 1") #=> "1"
68
- select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)").to_i
32
+ def connection
33
+ @connection ||= ConnectionWrapper.new(connection_class.connection)
69
34
  end
70
35
 
71
- def auto_increment_value(table)
72
- select_value(<<-SQL).to_i
73
- SELECT auto_increment
74
- FROM information_schema.tables
75
- WHERE table_name = '#{table}'
76
- AND table_schema = database()
77
- SQL
36
+ def tables_to_truncate(connection)
37
+ if @only.none?
38
+ all_tables = cache_tables? ? connection.database_cleaner_table_cache : connection.database_tables
39
+ @only = all_tables.map { |table| table.split(".").last }
40
+ end
41
+ @except += connection.database_cleaner_view_cache + migration_storage_names
42
+ @only - @except
78
43
  end
79
44
 
80
- # This method tells us if the given table has been inserted into since its
81
- # last truncation. Note that the table might have been populated, which
82
- # increased the auto-increment counter, but then cleaned again such that
83
- # it appears empty now.
84
- def has_been_used?(table)
85
- has_rows?(table) || auto_increment_value(table) > 1
45
+ def migration_storage_names
46
+ [
47
+ DatabaseCleaner::ActiveRecord::Base.migration_table_name,
48
+ ::ActiveRecord::Base.internal_metadata_table_name,
49
+ ]
86
50
  end
87
51
 
88
- def has_rows?(table)
89
- row_count(table) > 0
52
+ def cache_tables?
53
+ !!@cache_tables
90
54
  end
91
- end
92
55
 
93
- module IBM_DBAdapter
94
- def truncate_table(table_name)
95
- execute("TRUNCATE #{quote_table_name(table_name)} IMMEDIATE")
56
+ def pre_count?
57
+ @pre_count == true
96
58
  end
97
59
  end
98
60
 
99
- module SQLiteAdapter
100
- def delete_table(table_name)
101
- execute("DELETE FROM #{quote_table_name(table_name)};")
102
- if uses_sequence
103
- execute("DELETE FROM sqlite_sequence where name = '#{table_name}';")
61
+ class ConnectionWrapper < SimpleDelegator
62
+ def initialize(connection)
63
+ extend AbstractAdapter
64
+ case connection.adapter_name
65
+ when "Mysql2"
66
+ extend AbstractMysqlAdapter
67
+ when "SQLite"
68
+ extend AbstractMysqlAdapter
69
+ extend SQLiteAdapter
70
+ when "PostgreSQL"
71
+ extend AbstractMysqlAdapter
72
+ extend PostgreSQLAdapter
104
73
  end
74
+ super(connection)
105
75
  end
106
- alias truncate_table delete_table
107
76
 
108
- def truncate_tables(tables)
109
- tables.each { |t| truncate_table(t) }
110
- end
77
+ module AbstractAdapter
78
+ # used to be called views but that can clash with gems like schema_plus
79
+ # this gem is not meant to be exposing such an extra interface any way
80
+ def database_cleaner_view_cache
81
+ @views ||= select_values("select table_name from information_schema.views where table_schema = '#{current_database}'") rescue []
82
+ end
111
83
 
112
- private
84
+ def database_cleaner_table_cache
85
+ # the adapters don't do caching (#130) but we make the assumption that the list stays the same in tests
86
+ @database_cleaner_tables ||= database_tables
87
+ end
113
88
 
114
- # Returns a boolean indicating if the SQLite database is using the sqlite_sequence table.
115
- def uses_sequence
116
- select_value("SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_sequence';")
117
- end
118
- end
89
+ def database_tables
90
+ tables
91
+ end
119
92
 
120
- module TruncateOrDelete
121
- def truncate_table(table_name)
122
- begin
123
- execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
93
+ def truncate_table(table_name)
94
+ execute("TRUNCATE TABLE #{quote_table_name(table_name)}")
124
95
  rescue ::ActiveRecord::StatementInvalid
125
- execute("DELETE FROM #{quote_table_name(table_name)};")
96
+ execute("DELETE FROM #{quote_table_name(table_name)}")
126
97
  end
127
- end
128
- end
129
98
 
130
- module OracleAdapter
131
- def truncate_table(table_name)
132
- execute("TRUNCATE TABLE #{quote_table_name(table_name)}")
99
+ def truncate_tables(tables)
100
+ tables.each { |t| truncate_table(t) }
101
+ end
133
102
  end
134
- end
135
103
 
136
- module PostgreSQLAdapter
137
- def db_version
138
- @db_version ||= postgresql_version
139
- end
104
+ module AbstractMysqlAdapter
105
+ def pre_count_truncate_tables(tables)
106
+ truncate_tables(pre_count_tables(tables))
107
+ end
140
108
 
141
- def cascade
142
- @cascade ||= db_version >= 80200 ? 'CASCADE' : ''
143
- end
109
+ def pre_count_tables(tables)
110
+ tables.select { |table| has_been_used?(table) }
111
+ end
144
112
 
145
- def restart_identity
146
- @restart_identity ||= db_version >= 80400 ? 'RESTART IDENTITY' : ''
147
- end
113
+ private
148
114
 
149
- def truncate_table(table_name)
150
- truncate_tables([table_name])
151
- end
115
+ def row_count(table)
116
+ select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)")
117
+ end
152
118
 
153
- def truncate_tables(table_names)
154
- return if table_names.nil? || table_names.empty?
155
- execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}.join(', ')} #{restart_identity} #{cascade};")
156
- end
119
+ def auto_increment_value(table)
120
+ select_value(<<-SQL).to_i
121
+ SELECT auto_increment
122
+ FROM information_schema.tables
123
+ WHERE table_name = '#{table}'
124
+ AND table_schema = database()
125
+ SQL
126
+ end
157
127
 
158
- def pre_count_truncate_tables(tables, options = {:reset_ids => true})
159
- filter = options[:reset_ids] ? method(:has_been_used?) : method(:has_rows?)
160
- truncate_tables(tables.select(&filter))
161
- end
128
+ # This method tells us if the given table has been inserted into since its
129
+ # last truncation. Note that the table might have been populated, which
130
+ # increased the auto-increment counter, but then cleaned again such that
131
+ # it appears empty now.
132
+ def has_been_used?(table)
133
+ has_rows?(table) || auto_increment_value(table) > 1
134
+ end
162
135
 
163
- def database_cleaner_table_cache
164
- # AR returns a list of tables without schema but then returns a
165
- # migrations table with the schema. There are other problems, too,
166
- # with using the base list. If a table exists in multiple schemas
167
- # within the search path, truncation without the schema name could
168
- # result in confusing, if not unexpected results.
169
- @database_cleaner_tables ||= tables_with_schema
136
+ def has_rows?(table)
137
+ row_count(table) > 0
138
+ end
170
139
  end
171
140
 
172
- private
141
+ module SQLiteAdapter
142
+ def truncate_table(table_name)
143
+ super
144
+ if uses_sequence?
145
+ execute("DELETE FROM sqlite_sequence where name = '#{table_name}';")
146
+ end
147
+ end
173
148
 
174
- # Returns a boolean indicating if the given table has an auto-inc number higher than 0.
175
- # Note, this is different than an empty table since an table may populated, the index increased,
176
- # but then the table is cleaned. In other words, this function tells us if the given table
177
- # was ever inserted into.
178
- def has_been_used?(table)
179
- return has_rows?(table) unless has_sequence?(table)
149
+ def truncate_tables(tables)
150
+ tables.each { |t| truncate_table(t) }
151
+ end
180
152
 
181
- cur_val = select_value("SELECT currval('#{table}_id_seq');").to_i rescue 0
182
- cur_val > 0
183
- end
153
+ def pre_count_truncate_tables(tables)
154
+ truncate_tables(pre_count_tables(tables))
155
+ end
184
156
 
185
- def has_sequence?(table)
186
- select_value("SELECT true FROM pg_class WHERE relname = '#{table}_id_seq';")
187
- end
157
+ def pre_count_tables(tables)
158
+ sequences = fetch_sequences
159
+ tables.select { |table| has_been_used?(table, sequences) }
160
+ end
188
161
 
189
- def has_rows?(table)
190
- select_value("SELECT true FROM #{table} LIMIT 1;")
191
- end
162
+ private
192
163
 
193
- def tables_with_schema
194
- rows = select_rows <<-_SQL
195
- SELECT schemaname || '.' || tablename
196
- FROM pg_tables
197
- WHERE
198
- tablename !~ '_prt_' AND
199
- #{::DatabaseCleaner::ActiveRecord::Base.exclusion_condition('tablename')} AND
200
- schemaname = ANY (current_schemas(false))
201
- _SQL
202
- rows.collect { |result| result.first }
203
- end
204
- end
205
- end
206
- end
164
+ def fetch_sequences
165
+ return {} unless uses_sequence?
166
+ results = select_all("SELECT * FROM sqlite_sequence")
167
+ Hash[results.rows]
168
+ end
169
+
170
+ def has_been_used?(table, sequences)
171
+ count = sequences.fetch(table) { row_count(table) }
172
+ count > 0
173
+ end
207
174
 
208
- module ActiveRecord
209
- module ConnectionAdapters
210
- #Apply adapter decoraters where applicable (adapter should be loaded)
211
- AbstractAdapter.class_eval { include DatabaseCleaner::ConnectionAdapters::AbstractAdapter }
175
+ def row_count(table)
176
+ select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)")
177
+ end
212
178
 
213
- if defined?(JdbcAdapter)
214
- if defined?(OracleJdbcConnection)
215
- JdbcAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::OracleAdapter }
216
- else
217
- JdbcAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::TruncateOrDelete }
179
+ # Returns a boolean indicating if the SQLite database is using the sqlite_sequence table.
180
+ def uses_sequence?
181
+ select_value("SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_sequence';")
182
+ end
218
183
  end
219
- end
220
- AbstractMysqlAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(AbstractMysqlAdapter)
221
- Mysql2Adapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(Mysql2Adapter)
222
- MysqlAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(MysqlAdapter)
223
- SQLiteAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::SQLiteAdapter } if defined?(SQLiteAdapter)
224
- SQLite3Adapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::SQLiteAdapter } if defined?(SQLite3Adapter)
225
- PostgreSQLAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::PostgreSQLAdapter } if defined?(PostgreSQLAdapter)
226
- IBM_DBAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::IBM_DBAdapter } if defined?(IBM_DBAdapter)
227
- SQLServerAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::TruncateOrDelete } if defined?(SQLServerAdapter)
228
- OracleEnhancedAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::OracleAdapter } if defined?(OracleEnhancedAdapter)
229
- end
230
- end
231
184
 
232
- module DatabaseCleaner::ActiveRecord
233
- class Truncation
234
- include ::DatabaseCleaner::ActiveRecord::Base
235
- include ::DatabaseCleaner::Generic::Truncation
185
+ module PostgreSQLAdapter
186
+ def database_tables
187
+ tables_with_schema
188
+ end
236
189
 
237
- def initialize(*)
238
- super
239
- if !@reset_ids.nil?
240
- DatabaseCleaner.deprecate "The `:reset_ids` option for the ActiveRecord truncation strategy has no effect, and is deprecated. It will be removed in database_cleaner 2.0 with no replacement."
241
- end
242
- end
190
+ def truncate_tables(table_names)
191
+ return if table_names.nil? || table_names.empty?
192
+ execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}.join(', ')} RESTART IDENTITY CASCADE;")
193
+ end
243
194
 
244
- def clean
245
- connection = connection_class.connection
246
- connection.disable_referential_integrity do
247
- if pre_count? && connection.respond_to?(:pre_count_truncate_tables)
248
- connection.pre_count_truncate_tables(tables_to_truncate(connection), {:reset_ids => reset_ids?})
249
- else
250
- connection.truncate_tables(tables_to_truncate(connection))
195
+ def pre_count_truncate_tables(tables)
196
+ truncate_tables(pre_count_tables(tables))
251
197
  end
252
- end
253
- end
254
198
 
255
- private
199
+ def pre_count_tables(tables)
200
+ tables.select { |table| has_been_used?(table) }
201
+ end
256
202
 
257
- def tables_to_truncate(connection)
258
- tables_in_db = cache_tables? ? connection.database_cleaner_table_cache : connection.database_tables
259
- to_reject = (@tables_to_exclude + connection.database_cleaner_view_cache)
260
- (@only || tables_in_db).reject do |table|
261
- if ( m = table.match(/([^.]+)$/) )
262
- to_reject.include?(m[1])
263
- else
264
- false
203
+ def database_cleaner_table_cache
204
+ # AR returns a list of tables without schema but then returns a
205
+ # migrations table with the schema. There are other problems, too,
206
+ # with using the base list. If a table exists in multiple schemas
207
+ # within the search path, truncation without the schema name could
208
+ # result in confusing, if not unexpected results.
209
+ @database_cleaner_tables ||= tables_with_schema
265
210
  end
266
- end
267
- end
268
211
 
269
- # overwritten
270
- def migration_storage_names
271
- result = [::DatabaseCleaner::ActiveRecord::Base.migration_table_name]
272
- result << ::ActiveRecord::Base.internal_metadata_table_name if ::ActiveRecord::VERSION::MAJOR >= 5
273
- result
274
- end
212
+ private
275
213
 
276
- def cache_tables?
277
- !!@cache_tables
278
- end
214
+ # Returns a boolean indicating if the given table has an auto-inc number higher than 0.
215
+ # Note, this is different than an empty table since an table may populated, the index increased,
216
+ # but then the table is cleaned. In other words, this function tells us if the given table
217
+ # was ever inserted into.
218
+ def has_been_used?(table)
219
+ return has_rows?(table) unless has_sequence?(table)
279
220
 
280
- def pre_count?
281
- @pre_count == true
282
- end
221
+ cur_val = select_value("SELECT currval('#{table}_id_seq');").to_i rescue 0
222
+ cur_val > 0
223
+ end
224
+
225
+ def has_sequence?(table)
226
+ select_value("SELECT true FROM pg_class WHERE relname = '#{table}_id_seq';")
227
+ end
283
228
 
284
- def reset_ids?
285
- @reset_ids != false
229
+ def has_rows?(table)
230
+ select_value("SELECT true FROM #{table} LIMIT 1;")
231
+ end
232
+
233
+ def tables_with_schema
234
+ rows = select_rows <<-_SQL
235
+ SELECT schemaname || '.' || tablename
236
+ FROM pg_tables
237
+ WHERE
238
+ tablename !~ '_prt_' AND
239
+ #{DatabaseCleaner::ActiveRecord::Base.exclusion_condition('tablename')} AND
240
+ schemaname = ANY (current_schemas(false))
241
+ _SQL
242
+ rows.collect { |result| result.first }
243
+ end
244
+ end
286
245
  end
246
+ private_constant :ConnectionWrapper
287
247
  end
288
248
  end
249
+
@@ -1,5 +1,5 @@
1
1
  module DatabaseCleaner
2
2
  module ActiveRecord
3
- VERSION = "1.99.0"
3
+ VERSION = "2.0.0"
4
4
  end
5
5
  end
@@ -1,6 +1,7 @@
1
1
  require 'database_cleaner/active_record/version'
2
- require 'database_cleaner'
3
- require 'database_cleaner/active_record/deletion'
2
+ require 'database_cleaner/core'
4
3
  require 'database_cleaner/active_record/transaction'
5
4
  require 'database_cleaner/active_record/truncation'
5
+ require 'database_cleaner/active_record/deletion'
6
6
 
7
+ DatabaseCleaner[:active_record].strategy = :transaction