database_cleaner-active_record 1.8.0 → 2.0.0

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