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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.travis.yml +15 -2
- data/Appraisals +15 -0
- data/Gemfile +9 -4
- data/README.md +36 -43
- data/bin/setup +0 -1
- data/database_cleaner-active_record.gemspec +11 -20
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5.1.gemfile +14 -0
- data/gemfiles/rails_5.2.gemfile +14 -0
- data/gemfiles/rails_6.0.gemfile +14 -0
- data/gemfiles/rails_6.1.gemfile +14 -0
- data/lib/database_cleaner/active_record/base.rb +45 -55
- data/lib/database_cleaner/active_record/deletion.rb +49 -86
- data/lib/database_cleaner/active_record/transaction.rb +10 -47
- data/lib/database_cleaner/active_record/truncation.rb +184 -223
- data/lib/database_cleaner/active_record/version.rb +1 -1
- data/lib/database_cleaner/active_record.rb +3 -2
- metadata +30 -38
- data/Gemfile.lock +0 -73
@@ -1,59 +1,22 @@
|
|
1
1
|
require 'database_cleaner/active_record/base'
|
2
|
-
require 'database_cleaner/generic/transaction'
|
3
2
|
|
4
|
-
module DatabaseCleaner
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
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
|
-
|
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
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
50
|
-
|
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
|
-
|
55
|
-
|
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
|
59
|
-
|
60
|
-
|
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
|
66
|
-
|
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
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
89
|
-
|
52
|
+
def cache_tables?
|
53
|
+
!!@cache_tables
|
90
54
|
end
|
91
|
-
end
|
92
55
|
|
93
|
-
|
94
|
-
|
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
|
-
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
end
|
118
|
-
end
|
89
|
+
def database_tables
|
90
|
+
tables
|
91
|
+
end
|
119
92
|
|
120
|
-
|
121
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
99
|
+
def truncate_tables(tables)
|
100
|
+
tables.each { |t| truncate_table(t) }
|
101
|
+
end
|
133
102
|
end
|
134
|
-
end
|
135
103
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
104
|
+
module AbstractMysqlAdapter
|
105
|
+
def pre_count_truncate_tables(tables)
|
106
|
+
truncate_tables(pre_count_tables(tables))
|
107
|
+
end
|
140
108
|
|
141
|
-
|
142
|
-
|
143
|
-
|
109
|
+
def pre_count_tables(tables)
|
110
|
+
tables.select { |table| has_been_used?(table) }
|
111
|
+
end
|
144
112
|
|
145
|
-
|
146
|
-
@restart_identity ||= db_version >= 80400 ? 'RESTART IDENTITY' : ''
|
147
|
-
end
|
113
|
+
private
|
148
114
|
|
149
|
-
|
150
|
-
|
151
|
-
|
115
|
+
def row_count(table)
|
116
|
+
select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)")
|
117
|
+
end
|
152
118
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
164
|
-
|
165
|
-
|
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
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
153
|
+
def pre_count_truncate_tables(tables)
|
154
|
+
truncate_tables(pre_count_tables(tables))
|
155
|
+
end
|
184
156
|
|
185
|
-
|
186
|
-
|
187
|
-
|
157
|
+
def pre_count_tables(tables)
|
158
|
+
sequences = fetch_sequences
|
159
|
+
tables.select { |table| has_been_used?(table, sequences) }
|
160
|
+
end
|
188
161
|
|
189
|
-
|
190
|
-
select_value("SELECT true FROM #{table} LIMIT 1;")
|
191
|
-
end
|
162
|
+
private
|
192
163
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
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
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
233
|
-
|
234
|
-
|
235
|
-
|
185
|
+
module PostgreSQLAdapter
|
186
|
+
def database_tables
|
187
|
+
tables_with_schema
|
188
|
+
end
|
236
189
|
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
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
|
-
|
245
|
-
|
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
|
-
|
199
|
+
def pre_count_tables(tables)
|
200
|
+
tables.select { |table| has_been_used?(table) }
|
201
|
+
end
|
256
202
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
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
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
285
|
-
|
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,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
|