database_cleaner-active_record 1.99.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +80 -0
- data/.gitignore +5 -0
- data/.travis.yml +23 -2
- data/Appraisals +23 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +8 -4
- data/README.md +37 -44
- data/bin/setup +0 -1
- data/database_cleaner-active_record.gemspec +12 -21
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/rails_5.1.gemfile +13 -0
- data/gemfiles/rails_5.2.gemfile +13 -0
- data/gemfiles/rails_6.0.gemfile +13 -0
- data/gemfiles/rails_6.1.gemfile +20 -0
- data/gemfiles/rails_7.0.gemfile +13 -0
- data/gemfiles/rails_7.1.gemfile +13 -0
- data/lib/database_cleaner/active_record/base.rb +54 -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 +38 -42
- data/Gemfile.lock +0 -73
@@ -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", "PostGIS"
|
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 RESTRICT;")
|
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
|