database_cleaner-active_record 2.0.0.beta → 2.0.0.beta2
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/.travis.yml +2 -7
- data/Gemfile +6 -5
- data/README.md +35 -42
- data/database_cleaner-active_record.gemspec +2 -5
- data/gemfiles/rails_5.1.gemfile +7 -2
- data/gemfiles/rails_5.2.gemfile +7 -2
- data/gemfiles/rails_6.0.gemfile +7 -2
- data/lib/database_cleaner/active_record.rb +1 -1
- data/lib/database_cleaner/active_record/base.rb +26 -45
- data/lib/database_cleaner/active_record/deletion.rb +49 -86
- data/lib/database_cleaner/active_record/transaction.rb +10 -48
- data/lib/database_cleaner/active_record/truncation.rb +184 -219
- data/lib/database_cleaner/active_record/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 53199df54296cd17b30bf241e2ca431323b7bef7c4361240f63a682be6d02ee9
|
4
|
+
data.tar.gz: 668c5fbe4726841ff27034bcf81de0f98c6ba9494aa2a0cb3fcbade1bfb63d74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 253bced62511194095bef9671ed30602be05d2c417ba39e79dd7b2df7b58f5f602551ccd53c91ad451746bfb9e6086e107818d089c0b37256e4035098bb10fdf
|
7
|
+
data.tar.gz: 5a4b22a83f03b8064b77e966c277ae9ebc98ff9ca44c0bb229df817a6b21fc0853b454f7307ae195e4184281f0f6e4d2bfc207b2845fe95e358488c90de70f92
|
data/.travis.yml
CHANGED
@@ -3,7 +3,6 @@ services:
|
|
3
3
|
- mysql
|
4
4
|
- postgresql
|
5
5
|
rvm:
|
6
|
-
- 2.4
|
7
6
|
- 2.5
|
8
7
|
- 2.6
|
9
8
|
- 2.7
|
@@ -11,11 +10,7 @@ gemfile:
|
|
11
10
|
- gemfiles/rails_5.1.gemfile
|
12
11
|
- gemfiles/rails_5.2.gemfile
|
13
12
|
- gemfiles/rails_6.0.gemfile
|
14
|
-
jobs:
|
15
|
-
exclude:
|
16
|
-
- rvm: 2.4
|
17
|
-
gemfile: gemfiles/rails_6.0.gemfile
|
18
13
|
before_install:
|
19
|
-
- gem install bundler -v 1.17.2
|
20
14
|
- bin/setup
|
21
|
-
|
15
|
+
cache:
|
16
|
+
bundler: true
|
data/Gemfile
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
source "https://rubygems.org"
|
2
2
|
|
3
|
-
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
|
4
|
-
|
5
|
-
# Specify your gem's dependencies in database_cleaner-active_record.gemspec
|
6
3
|
gemspec
|
7
4
|
|
8
|
-
gem "
|
5
|
+
gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner"
|
9
6
|
|
10
7
|
gem "rails", "~>5.2"
|
8
|
+
gem "byebug"
|
11
9
|
|
12
|
-
|
10
|
+
group :test do
|
11
|
+
gem "simplecov", require: false
|
12
|
+
gem "codecov", require: false
|
13
|
+
end
|
data/README.md
CHANGED
@@ -2,11 +2,14 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/DatabaseCleaner/database_cleaner-active_record)
|
4
4
|
[](https://codeclimate.com/github/DatabaseCleaner/database_cleaner-active_record)
|
5
|
+
[](https://codecov.io/gh/DatabaseCleaner/database_cleaner-active_record)
|
5
6
|
|
6
7
|
Clean your ActiveRecord databases with Database Cleaner.
|
7
8
|
|
8
9
|
See https://github.com/DatabaseCleaner/database_cleaner for more information.
|
9
10
|
|
11
|
+
For support or to discuss development please use the [Google Group](https://groups.google.com/group/database_cleaner).
|
12
|
+
|
10
13
|
## Installation
|
11
14
|
|
12
15
|
```ruby
|
@@ -18,26 +21,11 @@ end
|
|
18
21
|
|
19
22
|
## Supported Strategies
|
20
23
|
|
21
|
-
|
22
|
-
|
23
|
-
<table>
|
24
|
-
<tbody>
|
25
|
-
<tr>
|
26
|
-
<th>Truncation</th>
|
27
|
-
<th>Transaction</th>
|
28
|
-
<th>Deletion</th>
|
29
|
-
</tr>
|
30
|
-
<tr>
|
31
|
-
<td> Yes</td>
|
32
|
-
<td> <b>Yes</b></td>
|
33
|
-
<td> Yes</td>
|
34
|
-
</tr>
|
35
|
-
</tbody>
|
36
|
-
</table>
|
37
|
-
|
38
|
-
(Default strategy is denoted in bold)
|
24
|
+
Three strategies are supported:
|
39
25
|
|
40
|
-
|
26
|
+
* Transaction (default)
|
27
|
+
* Truncation
|
28
|
+
* Deletion
|
41
29
|
|
42
30
|
## What strategy is fastest?
|
43
31
|
|
@@ -53,36 +41,41 @@ So what is fastest out of `:deletion` and `:truncation`? Well, it depends on you
|
|
53
41
|
|
54
42
|
Some people report much faster speeds with `:deletion` while others say `:truncation` is faster for them. The best approach therefore is it try all options on your test suite and see what is faster.
|
55
43
|
|
56
|
-
|
44
|
+
## Strategy configuration options
|
57
45
|
|
58
|
-
|
46
|
+
The transaction strategy accepts no options.
|
59
47
|
|
60
|
-
|
61
|
-
<tbody>
|
62
|
-
<tr>
|
63
|
-
<th>ORM</th>
|
64
|
-
<th>How to access</th>
|
65
|
-
<th>Notes</th>
|
66
|
-
</tr>
|
67
|
-
<tr>
|
68
|
-
<td> Active Record </td>
|
69
|
-
<td> <code>DatabaseCleaner[:active_record]</code></td>
|
70
|
-
<td> Connection specified as <code>:symbol</code> keys, loaded from <code>config/database.yml</code>. You may also pass in the ActiveRecord model under the <code>:model</code> key.</td>
|
71
|
-
</tr>
|
72
|
-
</tbody>
|
73
|
-
</table>
|
48
|
+
The truncation and deletion strategies may accept the following options:
|
74
49
|
|
75
|
-
|
50
|
+
* `:only` and `:except` may take a list of table names:
|
76
51
|
|
77
|
-
|
52
|
+
```ruby
|
53
|
+
# Only truncate the "users" table.
|
54
|
+
DatabaseCleaner[:active_record].strategy = :truncation, { only: ["users"] }
|
78
55
|
|
79
|
-
|
80
|
-
|
56
|
+
# Delete all tables except the "users" table.
|
57
|
+
DatabaseCleaner[:active_record].strategy = :deletion, { except: ["users"] }
|
58
|
+
```
|
81
59
|
|
82
|
-
|
60
|
+
* `:pre_count` - When set to `true` this will check each table for existing rows before truncating or deleting it. This can speed up test suites when many of the tables are never populated. Defaults to `false`. (Also, see the section on [What strategy is fastest?](#what-strategy-is-fastest))
|
83
61
|
|
84
62
|
* `:cache_tables` - When set to `true` the list of tables to truncate or delete from will only be read from the DB once, otherwise it will be read before each cleanup run. Set this to `false` if (1) you create and drop tables in your tests, or (2) you change Postgres schemas (`ActiveRecord::Base.connection.schema_search_path`) in your tests (for example, in a multitenancy setup with each tenant in a different Postgres schema). Defaults to `true`.
|
85
63
|
|
64
|
+
## Adapter configuration options
|
65
|
+
|
66
|
+
`#db` defaults to the default ActiveRecord database, but can be specified manually in a few ways:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# ActiveRecord connection key
|
70
|
+
DatabaseCleaner[:active_record].db = :logs
|
71
|
+
|
72
|
+
# Back to default:
|
73
|
+
DatabaseCleaner[:active_record].db = :default
|
74
|
+
|
75
|
+
# Multiple databases can be specified:
|
76
|
+
DatabaseCleaner[:active_record, connection: :default]
|
77
|
+
DatabaseCleaner[:active_record, connection: :logs]
|
78
|
+
```
|
86
79
|
|
87
80
|
## Common Errors
|
88
81
|
|
@@ -92,11 +85,11 @@ If you are using Postgres and have foreign key constraints, the truncation strat
|
|
92
85
|
|
93
86
|
To silence these warnings set the following log level in your `postgresql.conf` file:
|
94
87
|
|
95
|
-
```
|
88
|
+
```
|
96
89
|
client_min_messages = warning
|
97
90
|
```
|
98
91
|
|
99
|
-
|
92
|
+
You can also add this parameter to your database.yml file:
|
100
93
|
|
101
94
|
<pre>
|
102
95
|
test:
|
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path("../lib", __FILE__)
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require "database_cleaner/active_record/version"
|
1
|
+
require_relative "./lib/database_cleaner/active_record/version"
|
5
2
|
|
6
3
|
Gem::Specification.new do |spec|
|
7
4
|
spec.name = "database_cleaner-active_record"
|
@@ -21,7 +18,7 @@ Gem::Specification.new do |spec|
|
|
21
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
19
|
spec.require_paths = ["lib"]
|
23
20
|
|
24
|
-
spec.add_dependency "database_cleaner-core", "2.0.0.
|
21
|
+
spec.add_dependency "database_cleaner-core", "~>2.0.0.beta2"
|
25
22
|
spec.add_dependency "activerecord"
|
26
23
|
|
27
24
|
spec.add_development_dependency "bundler"
|
data/gemfiles/rails_5.1.gemfile
CHANGED
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "
|
5
|
+
gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner"
|
6
6
|
gem "rails", "~>5.1.0"
|
7
|
-
gem "
|
7
|
+
gem "byebug"
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "simplecov", require: false
|
11
|
+
gem "codecov", require: false
|
12
|
+
end
|
8
13
|
|
9
14
|
gemspec path: "../"
|
data/gemfiles/rails_5.2.gemfile
CHANGED
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "
|
5
|
+
gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner"
|
6
6
|
gem "rails", "~>5.2.0"
|
7
|
-
gem "
|
7
|
+
gem "byebug"
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "simplecov", require: false
|
11
|
+
gem "codecov", require: false
|
12
|
+
end
|
8
13
|
|
9
14
|
gemspec path: "../"
|
data/gemfiles/rails_6.0.gemfile
CHANGED
@@ -2,8 +2,13 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "
|
5
|
+
gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner"
|
6
6
|
gem "rails", "~>6.0.0"
|
7
|
-
gem "
|
7
|
+
gem "byebug"
|
8
|
+
|
9
|
+
group :test do
|
10
|
+
gem "simplecov", require: false
|
11
|
+
gem "codecov", require: false
|
12
|
+
end
|
8
13
|
|
9
14
|
gemspec path: "../"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'database_cleaner/active_record/version'
|
2
2
|
require 'database_cleaner/core'
|
3
|
-
require 'database_cleaner/active_record/deletion'
|
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
7
|
DatabaseCleaner[:active_record].strategy = :transaction
|
@@ -1,18 +1,10 @@
|
|
1
1
|
require 'active_record'
|
2
|
-
require 'database_cleaner/
|
2
|
+
require 'database_cleaner/strategy'
|
3
3
|
require 'erb'
|
4
4
|
require 'yaml'
|
5
5
|
|
6
6
|
module DatabaseCleaner
|
7
7
|
module ActiveRecord
|
8
|
-
def self.available_strategies
|
9
|
-
%w[truncation transaction deletion]
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.default_strategy
|
13
|
-
:transaction
|
14
|
-
end
|
15
|
-
|
16
8
|
def self.config_file_location=(path)
|
17
9
|
@config_file_location = path
|
18
10
|
end
|
@@ -21,64 +13,53 @@ module DatabaseCleaner
|
|
21
13
|
@config_file_location ||= "#{Dir.pwd}/config/database.yml"
|
22
14
|
end
|
23
15
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
attr_accessor :connection_hash
|
28
|
-
|
29
|
-
def db=(desired_db)
|
30
|
-
@db = desired_db
|
31
|
-
load_config
|
16
|
+
class Base < DatabaseCleaner::Strategy
|
17
|
+
def self.migration_table_name
|
18
|
+
::ActiveRecord::SchemaMigration.table_name
|
32
19
|
end
|
33
20
|
|
34
|
-
def
|
35
|
-
|
21
|
+
def self.exclusion_condition(column_name)
|
22
|
+
<<~SQL
|
23
|
+
#{column_name} <> '#{DatabaseCleaner::ActiveRecord::Base.migration_table_name}'
|
24
|
+
AND #{column_name} <> '#{::ActiveRecord::Base.internal_metadata_table_name}'
|
25
|
+
SQL
|
36
26
|
end
|
37
27
|
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
@connection_hash = valid_config(connection_details)[self.db.to_s]
|
42
|
-
end
|
28
|
+
def db=(*)
|
29
|
+
super
|
30
|
+
load_config
|
43
31
|
end
|
44
32
|
|
45
|
-
|
46
|
-
if !::ActiveRecord::Base.configurations.nil? && !::ActiveRecord::Base.configurations.empty?
|
47
|
-
if connection_file != ::ActiveRecord::Base.configurations
|
48
|
-
return ::ActiveRecord::Base.configurations
|
49
|
-
end
|
50
|
-
end
|
51
|
-
connection_file
|
52
|
-
end
|
33
|
+
attr_accessor :connection_hash
|
53
34
|
|
54
35
|
def connection_class
|
55
36
|
@connection_class ||= if db && !db.is_a?(Symbol)
|
56
37
|
db
|
57
38
|
elsif connection_hash
|
58
|
-
lookup_from_connection_pool || establish_connection
|
39
|
+
(lookup_from_connection_pool rescue nil) || establish_connection
|
59
40
|
else
|
60
41
|
::ActiveRecord::Base
|
61
42
|
end
|
62
43
|
end
|
63
44
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
::ActiveRecord
|
45
|
+
private
|
46
|
+
|
47
|
+
def load_config
|
48
|
+
if self.db != :default && self.db.is_a?(Symbol) && File.file?(DatabaseCleaner::ActiveRecord.config_file_location)
|
49
|
+
connection_details = YAML::load(ERB.new(IO.read(DatabaseCleaner::ActiveRecord.config_file_location)).result)
|
50
|
+
@connection_hash = valid_config(connection_details)[self.db.to_s]
|
69
51
|
end
|
70
52
|
end
|
71
53
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
|
54
|
+
def valid_config(connection_file)
|
55
|
+
if !::ActiveRecord::Base.configurations.nil? && !::ActiveRecord::Base.configurations.empty?
|
56
|
+
if connection_file != ::ActiveRecord::Base.configurations
|
57
|
+
return ::ActiveRecord::Base.configurations
|
58
|
+
end
|
76
59
|
end
|
77
|
-
|
60
|
+
connection_file
|
78
61
|
end
|
79
62
|
|
80
|
-
private
|
81
|
-
|
82
63
|
def lookup_from_connection_pool
|
83
64
|
if ::ActiveRecord::Base.respond_to?(:descendants)
|
84
65
|
database_name = connection_hash["database"] || connection_hash[:database]
|
@@ -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
|
8
|
-
|
9
|
-
def
|
10
|
-
|
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
|
-
|
15
|
-
def delete_table(table_name)
|
16
|
-
execute("DELETE FROM #{quote_table_name(table_name)};")
|
17
|
-
end
|
18
|
-
end
|
17
|
+
private
|
19
18
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
45
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
100
|
-
|
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,60 +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
|
-
private
|
55
|
-
|
56
|
-
def connection_maintains_transaction_count?
|
57
|
-
ActiveRecord::VERSION::MAJOR < 4
|
58
|
-
end
|
59
21
|
end
|
60
22
|
end
|
@@ -1,284 +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 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
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
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
|
-
|
49
|
-
|
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
|
-
|
54
|
-
|
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
|
58
|
-
|
59
|
-
|
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
|
65
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
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
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
88
|
-
|
52
|
+
def cache_tables?
|
53
|
+
!!@cache_tables
|
89
54
|
end
|
90
|
-
end
|
91
55
|
|
92
|
-
|
93
|
-
|
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
|
-
|
99
|
-
def
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
117
|
-
end
|
89
|
+
def database_tables
|
90
|
+
tables
|
91
|
+
end
|
118
92
|
|
119
|
-
|
120
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
99
|
+
def truncate_tables(tables)
|
100
|
+
tables.each { |t| truncate_table(t) }
|
101
|
+
end
|
132
102
|
end
|
133
|
-
end
|
134
103
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
104
|
+
module AbstractMysqlAdapter
|
105
|
+
def pre_count_truncate_tables(tables)
|
106
|
+
truncate_tables(pre_count_tables(tables))
|
107
|
+
end
|
139
108
|
|
140
|
-
|
141
|
-
|
142
|
-
|
109
|
+
def pre_count_tables(tables)
|
110
|
+
tables.select { |table| has_been_used?(table) }
|
111
|
+
end
|
143
112
|
|
144
|
-
|
145
|
-
@restart_identity ||= db_version >= 80400 ? 'RESTART IDENTITY' : ''
|
146
|
-
end
|
113
|
+
private
|
147
114
|
|
148
|
-
|
149
|
-
|
150
|
-
|
115
|
+
def row_count(table)
|
116
|
+
select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)")
|
117
|
+
end
|
151
118
|
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
155
127
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
160
135
|
|
161
|
-
|
162
|
-
|
163
|
-
|
136
|
+
def has_rows?(table)
|
137
|
+
row_count(table) > 0
|
138
|
+
end
|
164
139
|
end
|
165
140
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
end
|
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
|
174
148
|
|
175
|
-
|
149
|
+
def truncate_tables(tables)
|
150
|
+
tables.each { |t| truncate_table(t) }
|
151
|
+
end
|
176
152
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
# was ever inserted into.
|
181
|
-
def has_been_used?(table)
|
182
|
-
return has_rows?(table) unless has_sequence?(table)
|
153
|
+
def pre_count_truncate_tables(tables)
|
154
|
+
truncate_tables(pre_count_tables(tables))
|
155
|
+
end
|
183
156
|
|
184
|
-
|
185
|
-
|
186
|
-
|
157
|
+
def pre_count_tables(tables)
|
158
|
+
sequences = fetch_sequences
|
159
|
+
tables.select { |table| has_been_used?(table, sequences) }
|
160
|
+
end
|
187
161
|
|
188
|
-
|
189
|
-
select_value("SELECT true FROM pg_class WHERE relname = '#{table}_id_seq';")
|
190
|
-
end
|
162
|
+
private
|
191
163
|
|
192
|
-
|
193
|
-
|
194
|
-
|
164
|
+
def fetch_sequences
|
165
|
+
return {} unless uses_sequence?
|
166
|
+
results = select_all("SELECT * FROM sqlite_sequence")
|
167
|
+
Hash[results.rows]
|
168
|
+
end
|
195
169
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
WHERE
|
201
|
-
tablename !~ '_prt_' AND
|
202
|
-
#{::DatabaseCleaner::ActiveRecord::Base.exclusion_condition('tablename')} AND
|
203
|
-
schemaname = ANY (current_schemas(false))
|
204
|
-
_SQL
|
205
|
-
rows.collect { |result| result.first }
|
206
|
-
end
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
170
|
+
def has_been_used?(table, sequences)
|
171
|
+
count = sequences.fetch(table) { row_count(table) }
|
172
|
+
count > 0
|
173
|
+
end
|
210
174
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
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
|
215
178
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
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
|
221
183
|
end
|
222
|
-
end
|
223
|
-
AbstractMysqlAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(AbstractMysqlAdapter)
|
224
|
-
Mysql2Adapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(Mysql2Adapter)
|
225
|
-
MysqlAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::AbstractMysqlAdapter } if defined?(MysqlAdapter)
|
226
|
-
SQLiteAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::SQLiteAdapter } if defined?(SQLiteAdapter)
|
227
|
-
SQLite3Adapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::SQLiteAdapter } if defined?(SQLite3Adapter)
|
228
|
-
PostgreSQLAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::PostgreSQLAdapter } if defined?(PostgreSQLAdapter)
|
229
|
-
IBM_DBAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::IBM_DBAdapter } if defined?(IBM_DBAdapter)
|
230
|
-
SQLServerAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::TruncateOrDelete } if defined?(SQLServerAdapter)
|
231
|
-
OracleEnhancedAdapter.class_eval { include ::DatabaseCleaner::ConnectionAdapters::OracleAdapter } if defined?(OracleEnhancedAdapter)
|
232
|
-
end
|
233
|
-
end
|
234
184
|
|
235
|
-
module
|
236
|
-
|
237
|
-
|
238
|
-
|
185
|
+
module PostgreSQLAdapter
|
186
|
+
def database_tables
|
187
|
+
tables_with_schema
|
188
|
+
end
|
239
189
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
if pre_count? && connection.respond_to?(:pre_count_truncate_tables)
|
244
|
-
connection.pre_count_truncate_tables(tables_to_truncate(connection), {:reset_ids => reset_ids?})
|
245
|
-
else
|
246
|
-
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;")
|
247
193
|
end
|
248
|
-
end
|
249
|
-
end
|
250
194
|
|
251
|
-
|
195
|
+
def pre_count_truncate_tables(tables)
|
196
|
+
truncate_tables(pre_count_tables(tables))
|
197
|
+
end
|
252
198
|
|
253
|
-
|
254
|
-
|
255
|
-
to_reject = (@tables_to_exclude + connection.database_cleaner_view_cache)
|
256
|
-
(@only || tables_in_db).reject do |table|
|
257
|
-
if ( m = table.match(/([^.]+)$/) )
|
258
|
-
to_reject.include?(m[1])
|
259
|
-
else
|
260
|
-
false
|
199
|
+
def pre_count_tables(tables)
|
200
|
+
tables.select { |table| has_been_used?(table) }
|
261
201
|
end
|
262
|
-
end
|
263
|
-
end
|
264
202
|
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
271
211
|
|
272
|
-
|
273
|
-
!!@cache_tables
|
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)
|
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
|
279
228
|
|
280
|
-
|
281
|
-
|
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
|
282
245
|
end
|
246
|
+
private_constant :ConnectionWrapper
|
283
247
|
end
|
284
248
|
end
|
249
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: database_cleaner-active_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.
|
4
|
+
version: 2.0.0.beta2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ernesto Tagwerker
|
@@ -9,22 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2020-
|
12
|
+
date: 2020-05-30 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: database_cleaner-core
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- -
|
18
|
+
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: 2.0.0.
|
20
|
+
version: 2.0.0.beta2
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- -
|
25
|
+
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: 2.0.0.
|
27
|
+
version: 2.0.0.beta2
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: activerecord
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|