├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── database_cleaner-active_record.gemspec ├── gemfiles ├── .bundle │ └── config ├── rails_6.1.gemfile ├── rails_7.0.gemfile ├── rails_7.1.gemfile ├── rails_7.2.gemfile └── rails_edge.gemfile ├── lib ├── database_cleaner-active_record.rb └── database_cleaner │ ├── active_record.rb │ └── active_record │ ├── base.rb │ ├── deletion.rb │ ├── transaction.rb │ ├── truncation.rb │ └── version.rb ├── spec ├── database_cleaner │ ├── active_record │ │ ├── base_spec.rb │ │ ├── deletion_spec.rb │ │ ├── transaction_spec.rb │ │ └── truncation_spec.rb │ └── active_record_spec.rb ├── spec_helper.rb └── support │ ├── aliased-example.database.yml │ ├── database_helper.rb │ ├── example.database.yml │ └── sample.config.yml └── tmp └── .keep /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: 'Ruby: ${{ matrix.ruby }}, Rails: ${{ matrix.rails }}, Channel: ${{ matrix.channel }}' 8 | runs-on: 'ubuntu-22.04' 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | ruby: ['3.3', '3.2', '3.1'] 13 | rails: ['6.1', '7.0', '7.1', '7.2'] 14 | channel: ['stable'] 15 | 16 | include: 17 | - ruby: 'ruby-head' 18 | rails: 'edge' 19 | channel: 'experimental' 20 | - ruby: 'ruby-head' 21 | rails: '7.2' 22 | channel: 'experimental' 23 | - ruby: 'ruby-head' 24 | rails: '7.1' 25 | channel: 'experimental' 26 | 27 | - ruby: '3.3' 28 | rails: 'edge' 29 | channel: 'experimental' 30 | - ruby: '3.2' 31 | rails: 'edge' 32 | channel: 'experimental' 33 | - ruby: '3.1' 34 | rails: 'edge' 35 | channel: 'experimental' 36 | 37 | exclude: 38 | - ruby: '3.3' 39 | rails: '7.0' # TODO: works on 7-0-stable branch, remove after a 7.0.x patch release 40 | - ruby: '3.3' 41 | rails: '6.1' 42 | 43 | - ruby: '3.2' 44 | rails: '6.1' 45 | 46 | continue-on-error: ${{ matrix.channel != 'stable' }} 47 | 48 | env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps 49 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile 50 | steps: 51 | - uses: actions/checkout@v4 52 | - name: Set up Ruby ${{ matrix.ruby }} 53 | uses: ruby/setup-ruby@v1 54 | with: 55 | ruby-version: ${{ matrix.ruby }} 56 | bundler-cache: true # 'bundle install' and cache 57 | rubygems: ${{ matrix.ruby == '2.5' && 'default' || 'latest' }} 58 | - name: Copy config file 59 | run: cp spec/support/sample.config.yml spec/support/config.yml 60 | - name: Run tests 61 | run: bundle exec rake 62 | 63 | services: 64 | mysql: 65 | image: mysql:5.7 66 | env: 67 | MYSQL_ALLOW_EMPTY_PASSWORD: yes 68 | ports: 69 | - 3306:3306 70 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 71 | 72 | postgres: 73 | # Docker Hub image 74 | image: postgres 75 | # Provide the password for postgres 76 | env: 77 | POSTGRES_USER: postgres 78 | POSTGRES_PASSWORD: postgres 79 | ports: 80 | - 5432:5432 81 | # Set health checks to wait until postgres has started 82 | options: >- 83 | --health-cmd pg_isready 84 | --health-interval 10s 85 | --health-timeout 5s 86 | --health-retries 5 87 | 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/support/config.yml 9 | /tmp/ 10 | !/tmp/.keep 11 | /.ruby-version 12 | /.byebug_history 13 | 14 | # rspec failure tracking 15 | .rspec_status 16 | 17 | /Gemfile.lock 18 | /gemfiles/*.lock 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise "rails-6.1" do 2 | gem "rails", "~> 6.1.0" 3 | gem "sqlite3", "~> 1.5" 4 | end 5 | 6 | appraise "rails-7.0" do 7 | gem "rails", "~> 7.0.0" 8 | gem "sqlite3", "~> 1.7" 9 | end 10 | 11 | appraise "rails-7.1" do 12 | gem "rails", "~> 7.1.0" 13 | gem "sqlite3", "~> 1.7" # FIXME: remove after rails/rails#51592 14 | end 15 | 16 | appraise "rails-7.2" do 17 | gem "rails", "~> 7.2.0.beta2" 18 | end 19 | 20 | appraise "rails-edge" do 21 | gem "rails", github: "rails/rails" 22 | end 23 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Development (unreleased) 2 | 3 | ## v2.2.1 2025-05-13 4 | 5 | * https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/111 by @tagliala 6 | * https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/104 by @fatkodima 7 | * https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/118 by @pat, @thegeorgeous, and @nnishimura 8 | 9 | ## v2.2.0 2024-07-12 10 | 11 | * Fix "ERROR: currval of sequence" in Postgres adapter: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/103 12 | * Use lock synchronize on transaction callback: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/73 13 | * Stop testing with EOLed Ruby & Rails versions: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/105 14 | * Fix compatibility issue with Rails 7.2: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/107 15 | * Fix typo in truncation methods: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/94/files 16 | * Address deprecation of ActiveRecord::Base.connection in Rails 7.2: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/102 17 | * Support Rails 7.2+: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/101 18 | * Fix reset_ids test with Trilogy adapter: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/93 19 | * Implement resetting ids for deletion strategy: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/71 20 | * Avoid loading ActiveRecord::Base early: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/91 21 | * Fix specs to account for trilogy: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/88 22 | * Add basic support for trilogy: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/85 23 | 24 | ## v2.1.0 2023-02-17 25 | 26 | * Add Ruby 3.2 to CI matrix: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/79 27 | * Add Rails 7.1 support: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/78 28 | * Add WHERE clause to make `ruby-spanner-activerecord` happy: https://github.com/DatabaseCleaner/database_cleaner-active_record/pull/77 -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | 7 | gem "rails", "~>5.2" 8 | 9 | group :test do 10 | gem "simplecov", require: false 11 | gem "codecov", require: false 12 | end 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2009 Ben Mabey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Database Cleaner Adapter for ActiveRecord 2 | 3 | [![Tests](https://github.com/DatabaseCleaner/database_cleaner-active_record/actions/workflows/ci.yml/badge.svg)](https://github.com/DatabaseCleaner/database_cleaner-active_record/actions/workflows/ci.yml) 4 | [![Code Climate](https://codeclimate.com/github/DatabaseCleaner/database_cleaner-active_record/badges/gpa.svg)](https://codeclimate.com/github/DatabaseCleaner/database_cleaner-active_record) 5 | [![codecov](https://codecov.io/gh/DatabaseCleaner/database_cleaner-active_record/branch/master/graph/badge.svg)](https://codecov.io/gh/DatabaseCleaner/database_cleaner-active_record) 6 | 7 | Clean your ActiveRecord databases with Database Cleaner. 8 | 9 | See https://github.com/DatabaseCleaner/database_cleaner for more information. 10 | 11 | For support or to discuss development please use GitHub Issues. 12 | 13 | ## Installation 14 | 15 | ```ruby 16 | # Gemfile 17 | group :test do 18 | gem 'database_cleaner-active_record' 19 | end 20 | ``` 21 | 22 | ## Supported Strategies 23 | 24 | Three strategies are supported: 25 | 26 | * Transaction (default) 27 | * Truncation 28 | * Deletion 29 | 30 | ## What strategy is fastest? 31 | 32 | For the SQL libraries the fastest option will be to use `:transaction` as transactions are simply rolled back. If you can use this strategy you should. However, if you wind up needing to use multiple database connections in your tests (i.e. your tests run in a different process than your application) then using this strategy becomes a bit more difficult. You can get around the problem a number of ways. 33 | 34 | One common approach is to force all processes to use the same database connection ([common ActiveRecord hack](http://blog.plataformatec.com.br/2011/12/three-tips-to-improve-the-performance-of-your-test-suite/)) however this approach has been reported to result in non-deterministic failures. 35 | 36 | Another approach is to have the transactions rolled back in the application's process and relax the isolation level of the database (so the tests can read the uncommitted transactions). 37 | 38 | An easier, but slower, solution is to use the `:truncation` or `:deletion` strategy. 39 | 40 | So what is fastest out of `:deletion` and `:truncation`? Well, it depends on your table structure and what percentage of tables you populate in an average test. The reasoning is out of the scope of this README but here is a [good SO answer on this topic for Postgres](https://stackoverflow.com/questions/11419536/postgresql-truncation-speed/11423886#11423886). 41 | 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. 43 | 44 | ## Strategy configuration options 45 | 46 | The transaction strategy accepts no options. 47 | 48 | The truncation and deletion strategies may accept the following options: 49 | 50 | * `:only` and `:except` may take a list of table names: 51 | 52 | ```ruby 53 | # Only truncate the "users" table. 54 | DatabaseCleaner[:active_record].strategy = DatabaseCleaner::ActiveRecord::Truncation.new(only: ["users"]) 55 | 56 | # Delete all tables except the "users" table. 57 | DatabaseCleaner[:active_record].strategy = DatabaseCleaner::ActiveRecord::Deletion.new(except: ["users"]) 58 | ``` 59 | 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)) 61 | 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`. 63 | 64 | * `:reset_ids` - Only valid for deletion strategy, when set to `true` resets ids to 1 after each table is cleaned. 65 | 66 | * `:truncate_option` - Only valid for PostgreSQL. Acceptable values are `:restrict` and `:cascade`. Default is `:restrict` 67 | 68 | ## Adapter configuration options 69 | 70 | `#db` defaults to the default ActiveRecord database, but can be specified manually in a few ways: 71 | 72 | ```ruby 73 | # ActiveRecord connection key 74 | DatabaseCleaner[:active_record].db = :logs 75 | 76 | # Back to default: 77 | DatabaseCleaner[:active_record].db = :default 78 | 79 | # Multiple databases can be specified: 80 | DatabaseCleaner[:active_record, db: :default] 81 | DatabaseCleaner[:active_record, db: :logs] 82 | ``` 83 | 84 | ## Common Errors 85 | 86 | ### STDERR is being flooded when using Postgres 87 | 88 | If you are using Postgres and have foreign key constraints, the truncation strategy will cause a lot of extra noise to appear on STDERR (in the form of "NOTICE truncate cascades" messages). 89 | 90 | To silence these warnings set the following log level in your `postgresql.conf` file: 91 | 92 | ``` 93 | client_min_messages = warning 94 | ``` 95 | 96 | You can also add this parameter to your database.yml file: 97 | 98 |
 99 | test:
100 |   adapter: postgresql
101 |   # ...
102 |   min_messages: WARNING
103 | 
104 | 105 | ## COPYRIGHT 106 | 107 | See [LICENSE](LICENSE) for details. 108 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "database_cleaner/active_record" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle check || bundle install 7 | cp spec/support/sample.config.yml spec/support/config.yml 8 | -------------------------------------------------------------------------------- /database_cleaner-active_record.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "./lib/database_cleaner/active_record/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "database_cleaner-active_record" 5 | spec.version = DatabaseCleaner::ActiveRecord::VERSION 6 | spec.authors = ["Ernesto Tagwerker", "Micah Geisel"] 7 | spec.email = ["ernesto@ombulabs.com"] 8 | 9 | spec.summary = "Strategies for cleaning databases using ActiveRecord. Can be used to ensure a clean state for testing." 10 | spec.description = "Strategies for cleaning databases using ActiveRecord. Can be used to ensure a clean state for testing." 11 | spec.homepage = "https://github.com/DatabaseCleaner/database_cleaner-active_record" 12 | spec.license = "MIT" 13 | 14 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 15 | f.match(%r{^(test|spec|features)/}) 16 | end 17 | spec.bindir = "exe" 18 | spec.executables = [] 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "database_cleaner-core", "~>2.0.0" 22 | spec.add_dependency "activerecord", ">= 5.a" 23 | 24 | spec.add_development_dependency "bundler" 25 | spec.add_development_dependency "appraisal" 26 | spec.add_development_dependency "rake" 27 | spec.add_development_dependency "rspec" 28 | spec.add_development_dependency "mysql2" 29 | spec.add_development_dependency "pg" 30 | spec.add_development_dependency "sqlite3" 31 | spec.add_development_dependency "trilogy" 32 | end 33 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | gem "rails", "~> 6.1.0" 7 | gem "sqlite3", "~> 1.5" 8 | 9 | group :test do 10 | gem "simplecov", require: false 11 | gem "codecov", require: false 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | gem "rails", "~> 7.0.0" 7 | gem "sqlite3", "~> 1.7" 8 | 9 | group :test do 10 | gem "simplecov", require: false 11 | gem "codecov", require: false 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | gem "rails", "~> 7.1.0" 7 | gem "sqlite3", "~> 1.7" 8 | 9 | group :test do 10 | gem "simplecov", require: false 11 | gem "codecov", require: false 12 | end 13 | 14 | gemspec path: "../" 15 | -------------------------------------------------------------------------------- /gemfiles/rails_7.2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | gem "rails", "~> 7.2.0.beta2" 7 | 8 | group :test do 9 | gem "simplecov", require: false 10 | gem "codecov", require: false 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /gemfiles/rails_edge.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "database_cleaner-core", git: "https://github.com/DatabaseCleaner/database_cleaner" 6 | gem "rails", github: "rails/rails" 7 | 8 | group :test do 9 | gem "simplecov", require: false 10 | gem "codecov", require: false 11 | end 12 | 13 | gemspec path: "../" 14 | -------------------------------------------------------------------------------- /lib/database_cleaner-active_record.rb: -------------------------------------------------------------------------------- 1 | require "database_cleaner/active_record" 2 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'database_cleaner/core' 3 | 4 | ActiveSupport.on_load(:active_record) do 5 | require 'database_cleaner/active_record/base' 6 | require 'database_cleaner/active_record/transaction' 7 | require 'database_cleaner/active_record/truncation' 8 | require 'database_cleaner/active_record/deletion' 9 | 10 | DatabaseCleaner[:active_record].strategy = :transaction 11 | end 12 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record/base.rb: -------------------------------------------------------------------------------- 1 | require 'database_cleaner/strategy' 2 | require 'erb' 3 | require 'yaml' 4 | 5 | module DatabaseCleaner 6 | module ActiveRecord 7 | def self.config_file_location=(path) 8 | @config_file_location = path 9 | end 10 | 11 | def self.config_file_location 12 | @config_file_location ||= "#{Dir.pwd}/config/database.yml" 13 | end 14 | 15 | class Base < DatabaseCleaner::Strategy 16 | def self.migration_table_name 17 | if ::ActiveRecord::Base.connection_pool.respond_to?(:schema_migration) # Rails >= 7.2 18 | ::ActiveRecord::Base.connection_pool.schema_migration.table_name 19 | elsif ::ActiveRecord::Base.connection.respond_to?(:schema_migration) # Rails >= 6.0 20 | ::ActiveRecord::Base.connection.schema_migration.table_name 21 | else 22 | ::ActiveRecord::SchemaMigration.table_name 23 | end 24 | end 25 | 26 | def self.exclusion_condition(column_name) 27 | <<~SQL 28 | #{column_name} <> '#{DatabaseCleaner::ActiveRecord::Base.migration_table_name}' 29 | AND #{column_name} <> '#{::ActiveRecord::Base.internal_metadata_table_name}' 30 | SQL 31 | end 32 | 33 | def db=(*) 34 | super 35 | load_config 36 | end 37 | 38 | attr_accessor :connection_hash 39 | 40 | def connection_class 41 | @connection_class ||= if db && !db.is_a?(Symbol) 42 | db 43 | elsif connection_hash 44 | (lookup_from_connection_pool rescue nil) || establish_connection 45 | else 46 | ::ActiveRecord::Base 47 | end 48 | end 49 | 50 | private 51 | 52 | def load_config 53 | if db != :default && db.is_a?(Symbol) && File.file?(DatabaseCleaner::ActiveRecord.config_file_location) 54 | connection_details = 55 | if RUBY_VERSION.match?(/\A2\.5/) 56 | YAML.safe_load(ERB.new(IO.read(DatabaseCleaner::ActiveRecord.config_file_location)).result, [], [], true) 57 | else 58 | YAML.safe_load(ERB.new(IO.read(DatabaseCleaner::ActiveRecord.config_file_location)).result, aliases: true) 59 | end 60 | @connection_hash = valid_config(connection_details, db.to_s) 61 | end 62 | end 63 | 64 | def valid_config(connection_file, db) 65 | return connection_file[db] unless (active_record_config_hash = active_record_config_hash_for(db)) 66 | 67 | active_record_config_hash 68 | end 69 | 70 | def active_record_config_hash_for(db) 71 | if ::ActiveRecord.version >= Gem::Version.new('6.1') 72 | ::ActiveRecord::Base.configurations&.configs_for(name: db)&.configuration_hash 73 | else 74 | ::ActiveRecord::Base.configurations[db] 75 | end 76 | end 77 | 78 | def lookup_from_connection_pool 79 | return unless ::ActiveRecord::Base.respond_to?(:descendants) 80 | 81 | database_name = connection_hash['database'] || connection_hash[:database] 82 | ::ActiveRecord::Base.descendants.select(&:connection_pool).detect do |model| 83 | database_for(model) == database_name 84 | end 85 | end 86 | 87 | def establish_connection 88 | ::ActiveRecord::Base.establish_connection(connection_hash) 89 | ::ActiveRecord::Base 90 | end 91 | 92 | def database_for(model) 93 | if model.connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1 94 | model.connection_pool.db_config.configuration_hash[:database] 95 | else 96 | model.connection_pool.spec.config[:database] 97 | end 98 | end 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record/deletion.rb: -------------------------------------------------------------------------------- 1 | module DatabaseCleaner 2 | module ActiveRecord 3 | class Deletion < Truncation 4 | def clean 5 | connection.disable_referential_integrity do 6 | if pre_count? && connection.respond_to?(:pre_count_tables) 7 | delete_tables(connection, connection.pre_count_tables(tables_to_clean(connection))) 8 | else 9 | delete_tables(connection, tables_to_clean(connection)) 10 | end 11 | end 12 | end 13 | 14 | private 15 | 16 | def delete_tables(connection, table_names) 17 | table_names.each do |table_name| 18 | delete_table(connection, table_name) 19 | reset_id_sequence(connection, table_name) if @reset_ids 20 | end 21 | end 22 | 23 | def delete_table connection, table_name 24 | connection.execute("DELETE FROM #{connection.quote_table_name(table_name)} WHERE 1=1") 25 | end 26 | 27 | def reset_id_sequence connection, table_name 28 | case connection.adapter_name 29 | when 'Mysql2', 'Trilogy' 30 | connection.execute("ALTER TABLE #{table_name} AUTO_INCREMENT = 1;") 31 | when 'SQLite' 32 | connection.execute("delete from sqlite_sequence where name='#{table_name}';") 33 | when 'PostgreSQL' 34 | connection.reset_pk_sequence!(table_name) 35 | else 36 | raise "reset_id option not supported for #{connection.adapter_name}" 37 | end 38 | end 39 | 40 | def tables_to_clean(connection) 41 | if information_schema_exists?(connection) 42 | @except += connection.database_cleaner_view_cache + migration_storage_names 43 | (@only.any? ? @only : tables_with_new_rows(connection)) - @except 44 | else 45 | super 46 | end 47 | end 48 | 49 | def tables_with_new_rows(connection) 50 | stats = table_stats_query(connection) 51 | if stats != '' 52 | connection.select_values(stats) 53 | else 54 | [] 55 | end 56 | end 57 | 58 | def table_stats_query(connection) 59 | @table_stats_query ||= build_table_stats_query(connection) 60 | ensure 61 | @table_stats_query = nil unless @cache_tables 62 | end 63 | 64 | def build_table_stats_query(connection) 65 | tables = connection.select_values(<<-SQL) 66 | SELECT table_name 67 | FROM information_schema.tables 68 | WHERE table_schema = database() 69 | AND #{self.class.exclusion_condition('table_name')}; 70 | SQL 71 | queries = tables.map do |table| 72 | "(SELECT #{connection.quote(table)} FROM #{connection.quote_table_name(table)} LIMIT 1)" 73 | end 74 | queries.join(' UNION ALL ') 75 | end 76 | 77 | def information_schema_exists? connection 78 | ["Mysql2", "Trilogy"].include?(connection.adapter_name) 79 | end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record/transaction.rb: -------------------------------------------------------------------------------- 1 | module DatabaseCleaner 2 | module ActiveRecord 3 | class Transaction < Base 4 | def start 5 | connection = if ::ActiveRecord.version >= Gem::Version.new("7.2") 6 | connection_class.lease_connection 7 | else 8 | connection_class.connection 9 | end 10 | 11 | # Hack to make sure that the connection is properly set up before cleaning 12 | connection.transaction {} 13 | 14 | connection.begin_transaction joinable: false 15 | end 16 | 17 | 18 | def clean 19 | connection_class.connection_pool.connections.each do |connection| 20 | connection.lock.synchronize do 21 | next unless connection.open_transactions > 0 22 | connection.rollback_transaction 23 | end 24 | end 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record/truncation.rb: -------------------------------------------------------------------------------- 1 | require "delegate" 2 | require 'database_cleaner/active_record/base' 3 | 4 | module DatabaseCleaner 5 | module ActiveRecord 6 | class Truncation < Base 7 | def initialize(opts={}) 8 | if !opts.empty? && !(opts.keys - [:only, :except, :pre_count, :cache_tables, :reset_ids, :truncate_option]).empty? 9 | raise ArgumentError, "The only valid options are :only, :except, :pre_count, :reset_ids, :cache_tables and :truncate_option. You specified #{opts.keys.join(',')}." 10 | end 11 | 12 | @only = Array(opts[:only]).dup 13 | @except = Array(opts[:except]).dup 14 | 15 | @reset_ids = opts[:reset_ids] 16 | @pre_count = opts[:pre_count] 17 | @truncate_option = opts[:truncate_option] || :restrict 18 | @cache_tables = opts.has_key?(:cache_tables) ? !!opts[:cache_tables] : true 19 | end 20 | 21 | def clean 22 | connection.disable_referential_integrity do 23 | if pre_count? && connection.respond_to?(:pre_count_truncate_tables) 24 | connection.pre_count_truncate_tables(tables_to_clean(connection)) 25 | else 26 | connection.truncate_tables(tables_to_clean(connection), { truncate_option: @truncate_option }) 27 | end 28 | end 29 | end 30 | 31 | private 32 | 33 | def connection 34 | @connection ||= ConnectionWrapper.new( 35 | if ::ActiveRecord.version >= Gem::Version.new("7.2") 36 | connection_class.lease_connection 37 | else 38 | connection_class.connection 39 | end 40 | ) 41 | end 42 | 43 | def tables_to_clean(connection) 44 | if @only.none? 45 | all_tables = cache_tables? ? connection.database_cleaner_table_cache : connection.database_tables 46 | @only = all_tables.map { |table| table.split(".").last } 47 | end 48 | @except += connection.database_cleaner_view_cache + migration_storage_names 49 | @only - @except 50 | end 51 | 52 | def migration_storage_names 53 | [ 54 | DatabaseCleaner::ActiveRecord::Base.migration_table_name, 55 | ::ActiveRecord::Base.internal_metadata_table_name, 56 | ] 57 | end 58 | 59 | def cache_tables? 60 | !!@cache_tables 61 | end 62 | 63 | def pre_count? 64 | @pre_count == true 65 | end 66 | end 67 | 68 | class ConnectionWrapper < SimpleDelegator 69 | def initialize(connection) 70 | extend AbstractAdapter 71 | case connection.adapter_name 72 | when "Mysql2", "Trilogy" 73 | extend AbstractMysqlAdapter 74 | when "SQLite" 75 | extend AbstractMysqlAdapter 76 | extend SQLiteAdapter 77 | when "PostgreSQL", "PostGIS" 78 | extend AbstractMysqlAdapter 79 | extend PostgreSQLAdapter 80 | end 81 | super(connection) 82 | end 83 | 84 | module AbstractAdapter 85 | # used to be called views but that can clash with gems like schema_plus 86 | # this gem is not meant to be exposing such an extra interface any way 87 | def database_cleaner_view_cache 88 | @views ||= select_values("select table_name from information_schema.views where table_schema = '#{current_database}'") rescue [] 89 | end 90 | 91 | def database_cleaner_table_cache 92 | # the adapters don't do caching (#130) but we make the assumption that the list stays the same in tests 93 | @database_cleaner_tables ||= database_tables 94 | end 95 | 96 | def database_tables 97 | tables 98 | end 99 | 100 | def truncate_table(table_name) 101 | execute("TRUNCATE TABLE #{quote_table_name(table_name)}") 102 | rescue ::ActiveRecord::StatementInvalid 103 | execute("DELETE FROM #{quote_table_name(table_name)}") 104 | end 105 | 106 | def truncate_tables(tables, opts) 107 | tables.each { |t| truncate_table(t) } 108 | end 109 | end 110 | 111 | module AbstractMysqlAdapter 112 | def pre_count_truncate_tables(tables) 113 | truncate_tables(pre_count_tables(tables)) 114 | end 115 | 116 | def pre_count_tables(tables) 117 | tables.select { |table| has_been_used?(table) } 118 | end 119 | 120 | private 121 | 122 | def row_count(table) 123 | select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)") 124 | end 125 | 126 | def auto_increment_value(table) 127 | select_value(<<-SQL).to_i 128 | SELECT auto_increment 129 | FROM information_schema.tables 130 | WHERE table_name = '#{table}' 131 | AND table_schema = database() 132 | SQL 133 | end 134 | 135 | # This method tells us if the given table has been inserted into since its 136 | # last truncation. Note that the table might have been populated, which 137 | # increased the auto-increment counter, but then cleaned again such that 138 | # it appears empty now. 139 | def has_been_used?(table) 140 | has_rows?(table) || auto_increment_value(table) > 1 141 | end 142 | 143 | def has_rows?(table) 144 | row_count(table) > 0 145 | end 146 | end 147 | 148 | module SQLiteAdapter 149 | def truncate_table(table_name) 150 | super 151 | if uses_sequence? 152 | execute("DELETE FROM sqlite_sequence where name = '#{table_name}';") 153 | end 154 | end 155 | 156 | def truncate_tables(tables, opts) 157 | tables.each { |t| truncate_table(t) } 158 | end 159 | 160 | def pre_count_truncate_tables(tables) 161 | truncate_tables(pre_count_tables(tables)) 162 | end 163 | 164 | def pre_count_tables(tables) 165 | sequences = fetch_sequences 166 | tables.select { |table| has_been_used?(table, sequences) } 167 | end 168 | 169 | private 170 | 171 | def fetch_sequences 172 | return {} unless uses_sequence? 173 | results = select_all("SELECT * FROM sqlite_sequence") 174 | Hash[results.rows] 175 | end 176 | 177 | def has_been_used?(table, sequences) 178 | count = sequences.fetch(table) { row_count(table) } 179 | count > 0 180 | end 181 | 182 | def row_count(table) 183 | select_value("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table)} LIMIT 1)") 184 | end 185 | 186 | # Returns a boolean indicating if the SQLite database is using the sqlite_sequence table. 187 | def uses_sequence? 188 | select_value("SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_sequence';") 189 | end 190 | end 191 | 192 | module PostgreSQLAdapter 193 | def database_tables 194 | tables_with_schema 195 | end 196 | 197 | def truncate_tables(table_names, opts) 198 | return if table_names.nil? || table_names.empty? 199 | 200 | execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}.join(', ')} RESTART IDENTITY #{opts[:truncate_option]};") 201 | end 202 | 203 | def pre_count_truncate_tables(tables) 204 | truncate_tables(pre_count_tables(tables)) 205 | end 206 | 207 | def pre_count_tables(tables) 208 | tables.select { |table| has_been_used?(table) } 209 | end 210 | 211 | def database_cleaner_table_cache 212 | # AR returns a list of tables without schema but then returns a 213 | # migrations table with the schema. There are other problems, too, 214 | # with using the base list. If a table exists in multiple schemas 215 | # within the search path, truncation without the schema name could 216 | # result in confusing, if not unexpected results. 217 | @database_cleaner_tables ||= tables_with_schema 218 | end 219 | 220 | private 221 | 222 | # Returns a boolean indicating if the given table has an auto-inc number higher than 0. 223 | # Note, this is different than an empty table since an table may populated, the index increased, 224 | # but then the table is cleaned. In other words, this function tells us if the given table 225 | # was ever inserted into. 226 | def has_been_used?(table) 227 | return has_rows?(table) unless has_sequence?(table) 228 | 229 | cur_val = select_value("SELECT last_value from #{table}_id_seq;").to_i 230 | cur_val > 0 231 | end 232 | 233 | def has_sequence?(table) 234 | select_value("SELECT true FROM pg_class WHERE relname = '#{table}_id_seq';") 235 | end 236 | 237 | def has_rows?(table) 238 | select_value("SELECT true FROM #{table} LIMIT 1;") 239 | end 240 | 241 | def tables_with_schema 242 | rows = select_rows <<-_SQL 243 | SELECT schemaname || '.' || tablename 244 | FROM pg_tables 245 | WHERE 246 | tablename !~ '_prt_' AND 247 | #{DatabaseCleaner::ActiveRecord::Base.exclusion_condition('tablename')} AND 248 | schemaname = ANY (current_schemas(false)) 249 | _SQL 250 | rows.collect { |result| result.first } 251 | end 252 | end 253 | end 254 | private_constant :ConnectionWrapper 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /lib/database_cleaner/active_record/version.rb: -------------------------------------------------------------------------------- 1 | module DatabaseCleaner 2 | module ActiveRecord 3 | VERSION = "2.2.1" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/database_cleaner/active_record/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'database_cleaner/active_record/base' 3 | require 'database_cleaner/spec' 4 | require './spec/support/database_helper' 5 | 6 | module DatabaseCleaner 7 | module ActiveRecord 8 | RSpec.describe Base do 9 | subject(:strategy) { described_class.new } 10 | 11 | let(:config_location) { '/path/to/config/database.yml' } 12 | 13 | around do |example| 14 | DatabaseCleaner::ActiveRecord.config_file_location = config_location 15 | example.run 16 | DatabaseCleaner::ActiveRecord.config_file_location = nil 17 | end 18 | 19 | it_behaves_like 'a database_cleaner strategy' 20 | 21 | describe "db" do 22 | it "should store my desired db" do 23 | strategy.db = :my_db 24 | expect(strategy.db).to eq :my_db 25 | end 26 | 27 | it "should default to :default" do 28 | expect(strategy.db).to eq :default 29 | end 30 | end 31 | 32 | describe '#db=' do 33 | let(:my_db) { :my_db } 34 | let(:config_location) { 'spec/support/example.database.yml' } 35 | 36 | it 'should process erb in the config' do 37 | strategy.db = my_db 38 | expect(strategy.connection_hash).to eq({ 'database' => 'one' }) 39 | end 40 | 41 | context 'when the configs are aliased' do 42 | let(:other_db) { :other_db } 43 | let(:config_location) { 'spec/support/aliased-example.database.yml' } 44 | 45 | it 'loads configs correctly' do 46 | strategy.db = my_db 47 | expected_hash = { 'database' => 'one', 48 | 'encoding' => 'utf8', 49 | 'reconnect' => true } 50 | expect(strategy.connection_hash).to eq(expected_hash) 51 | 52 | strategy.db = other_db 53 | expected_hash.merge!('database' => 'two') 54 | expect(strategy.connection_hash).to eq(expected_hash) 55 | end 56 | end 57 | 58 | context 'when ActiveRecord configuration contains a config for the given db' do 59 | if ::ActiveRecord.version >= Gem::Version.new('6.1') 60 | context 'ActiveRecord >= 6.1' do 61 | before do 62 | allow(::ActiveRecord::Base) 63 | .to receive(:configurations).and_return(ac_db_configurations_mock) 64 | allow(ac_db_configurations_mock) 65 | .to receive(:configs_for).with({ name: my_db.to_s }).and_return(hash_config_mock) 66 | end 67 | 68 | let(:ac_db_configurations_mock) do 69 | instance_double(::ActiveRecord::DatabaseConfigurations) 70 | end 71 | let(:hash_config_mock) do 72 | instance_double( 73 | ::ActiveRecord::DatabaseConfigurations::HashConfig, 74 | configuration_hash: configuration_hash 75 | ) 76 | end 77 | let(:configuration_hash) { { 'database' => 'two'} } 78 | 79 | it 'uses the ActiveRecord configuration' do 80 | strategy.db = my_db 81 | expect(strategy.connection_hash).to eq(configuration_hash) 82 | end 83 | end 84 | else 85 | context 'ActiveRecord < 6.1' do 86 | before do 87 | allow(::ActiveRecord::Base) 88 | .to receive(:configurations).and_return(configurations_hash) 89 | end 90 | let(:configurations_hash) { { my_db.to_s => configuration_hash } } 91 | let(:configuration_hash) { { 'database' => 'two' } } 92 | 93 | it 'uses the ActiveRecord configuration' do 94 | strategy.db = my_db 95 | expect(strategy.connection_hash).to eq(configuration_hash) 96 | end 97 | end 98 | end 99 | end 100 | 101 | context 'when both the config file and ActiveRecord config are not available' do 102 | before do 103 | allow(File).to receive(:file?).with(config_location).and_return(false) 104 | end 105 | 106 | it 'skips the config' do 107 | strategy.db = my_db 108 | expect(strategy.connection_hash).not_to be 109 | end 110 | end 111 | 112 | context 'when the model is set' do 113 | it 'skips the config' do 114 | strategy.db = double(:model_class) 115 | expect(strategy.connection_hash).not_to be 116 | end 117 | end 118 | 119 | context 'when the db is set to :default' do 120 | it 'skips the config' do 121 | strategy.db = :default 122 | expect(strategy.connection_hash).not_to be 123 | end 124 | end 125 | end 126 | 127 | describe "connection_class" do 128 | it "should default to ActiveRecord::Base" do 129 | expect(strategy.connection_class).to eq ::ActiveRecord::Base 130 | end 131 | 132 | context "with database models" do 133 | let(:model_class) { double } 134 | 135 | context "connection_hash is set" do 136 | it "reuses the model's connection" do 137 | strategy.connection_hash = {} 138 | strategy.db = model_class 139 | expect(strategy.connection_class).to eq model_class 140 | end 141 | end 142 | 143 | context "connection_hash is not set" do 144 | it "reuses the model's connection" do 145 | strategy.db = model_class 146 | expect(strategy.connection_class).to eq model_class 147 | end 148 | end 149 | end 150 | 151 | context "when connection_hash is set" do 152 | let(:helper) { DatabaseHelper.new(:sqlite3) } 153 | let(:hash) { helper.send(:default_config) } 154 | 155 | around do |example| 156 | helper.setup 157 | strategy.connection_hash = hash 158 | example.run 159 | helper.teardown 160 | end 161 | 162 | context "and there are no models" do 163 | before do 164 | allow(::ActiveRecord::Base).to receive(:descendants).and_return([]) 165 | end 166 | 167 | it "establishes a connection with it" do 168 | expect(::ActiveRecord::Base).to receive(:establish_connection).with(hash) 169 | expect(strategy.connection_class).to eq ::ActiveRecord::Base 170 | end 171 | end 172 | 173 | context "and there are models" do 174 | 175 | it "fetches from connection pool" do 176 | expect(["Kernel::Agent", "Kernel::User"]).to include(strategy.connection_class.to_s) 177 | end 178 | end 179 | end 180 | end 181 | end 182 | end 183 | end 184 | -------------------------------------------------------------------------------- /spec/database_cleaner/active_record/deletion_spec.rb: -------------------------------------------------------------------------------- 1 | require 'support/database_helper' 2 | require 'database_cleaner/active_record/deletion' 3 | 4 | RSpec.describe DatabaseCleaner::ActiveRecord::Deletion do 5 | subject(:strategy) { described_class.new } 6 | 7 | DatabaseHelper.with_all_dbs do |helper| 8 | context "using a #{helper.db} connection" do 9 | around do |example| 10 | helper.setup 11 | example.run 12 | helper.teardown 13 | end 14 | 15 | let(:connection) { helper.connection } 16 | 17 | describe '#clean' do 18 | before do 19 | # Clean should not try to delete from database views. If it does, it will raise an error 20 | connection.execute "CREATE VIEW view1 AS SELECT * FROM schema_migrations;" 21 | end 22 | 23 | after do 24 | connection.execute "DROP VIEW view1" 25 | end 26 | 27 | context "with records" do 28 | before do 29 | 2.times { User.create! } 30 | 2.times { Agent.create! } 31 | end 32 | 33 | it "should delete from all tables" do 34 | expect { strategy.clean } 35 | .to change { [User.count, Agent.count] } 36 | .from([2,2]) 37 | .to([0,0]) 38 | end 39 | 40 | it "should not reset AUTO_INCREMENT index of table" do 41 | strategy.clean 42 | expect(User.create.id).to eq 3 43 | end 44 | 45 | context "reset_ids option set to true" do 46 | subject(:strategy) { described_class.new(reset_ids: true) } 47 | it "should reset AUTO_INCREMENT index of table" do 48 | strategy.clean 49 | expect(User.create.id).to eq 1 50 | end 51 | end 52 | 53 | it "should delete from all tables except for schema_migrations" do 54 | expect { strategy.clean } 55 | .to_not change { connection.select_value("select count(*) from schema_migrations;").to_i } 56 | .from(2) 57 | end 58 | 59 | it "should only delete from the tables specified in the :only option when provided" do 60 | expect { described_class.new(only: ['agents']).clean } 61 | .to change { [User.count, Agent.count] } 62 | .from([2,2]) 63 | .to([2,0]) 64 | end 65 | 66 | it "should not delete from the tables specified in the :except option" do 67 | expect { described_class.new(except: ['users']).clean } 68 | .to change { [User.count, Agent.count] } 69 | .from([2,2]) 70 | .to([2,0]) 71 | end 72 | 73 | it "should raise an error when invalid options are provided" do 74 | expect { described_class.new(foo: 'bar') }.to raise_error(ArgumentError) 75 | end 76 | 77 | it "should not delete from views" do 78 | allow(strategy.send(:connection)).to receive(:database_cleaner_view_cache).and_return(["users"]) 79 | 80 | expect { strategy.clean } 81 | .to change { [User.count, Agent.count] } 82 | .from([2,2]) 83 | .to([2,0]) 84 | end 85 | end 86 | 87 | describe "with pre_count optimization option" do 88 | subject(:strategy) { described_class.new(pre_count: true) } 89 | 90 | it "only delete from non-empty tables" do 91 | User.create! 92 | 93 | expect(strategy).to receive(:delete_table).with(strategy.send(:connection), 'users') 94 | strategy.clean 95 | end 96 | end 97 | 98 | context 'when :cache_tables is set to true' do 99 | subject(:strategy) { described_class.new(cache_tables: true) } 100 | 101 | it 'caches the list of tables to be deleted from' do 102 | if [:mysql2, :trilogy].include?(helper.db) 103 | expect(strategy).to receive(:build_table_stats_query).once.and_return("") 104 | elsif helper.db == :postgres 105 | expect(strategy.send(:connection)).to receive(:tables_with_schema).once.and_return([]) 106 | else 107 | expect(strategy.send(:connection)).to receive(:database_tables).once.and_return([]) 108 | end 109 | 110 | strategy.clean 111 | strategy.clean 112 | end 113 | end 114 | 115 | context 'when :cache_tables is set to false' do 116 | subject(:strategy) { described_class.new(cache_tables: false) } 117 | 118 | it 'does not cache the list of tables to be deleted from' do 119 | if [:mysql2, :trilogy].include?(helper.db) 120 | expect(strategy).to receive(:build_table_stats_query).twice.and_return("") 121 | elsif helper.db == :postgres 122 | expect(strategy.send(:connection)).to receive(:tables_with_schema).twice.and_return([]) 123 | else 124 | expect(strategy.send(:connection)).to receive(:database_tables).twice.and_return([]) 125 | end 126 | 127 | strategy.clean 128 | strategy.clean 129 | end 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/database_cleaner/active_record/transaction_spec.rb: -------------------------------------------------------------------------------- 1 | require 'support/database_helper' 2 | require 'database_cleaner/active_record/transaction' 3 | 4 | RSpec.describe DatabaseCleaner::ActiveRecord::Transaction do 5 | subject(:strategy) { described_class.new } 6 | 7 | DatabaseHelper.with_all_dbs do |helper| 8 | context "using a #{helper.db} connection" do 9 | around do |example| 10 | helper.setup 11 | example.run 12 | helper.teardown 13 | end 14 | 15 | describe "#clean" do 16 | context "after an initial #start" do 17 | before do 18 | strategy.start 19 | 2.times { User.create! } 20 | 2.times { Agent.create! } 21 | end 22 | 23 | it "should clean all tables" do 24 | expect { strategy.clean } 25 | .to change { [User.count, Agent.count] } 26 | .from([2,2]) 27 | .to([0,0]) 28 | end 29 | end 30 | 31 | context "with fixtures before an initial #start" do 32 | before do 33 | 2.times { User.create! } 34 | strategy.start 35 | 2.times { Agent.create! } 36 | end 37 | 38 | it "should not clean fixtures" do 39 | expect { strategy.clean } 40 | .to change { [User.count, Agent.count] } 41 | .from([2,2]) 42 | .to([2,0]) 43 | end 44 | end 45 | 46 | context "without an initial start" do 47 | before do 48 | 2.times { User.create! } 49 | 2.times { Agent.create! } 50 | end 51 | 52 | it "does nothing" do 53 | expect { strategy.clean } 54 | .to_not change { [User.count, Agent.count] } 55 | end 56 | end 57 | end 58 | 59 | describe "#cleaning" do 60 | context "with records" do 61 | it "should clean all tables" do 62 | strategy.cleaning do 63 | 2.times { User.create! } 64 | 2.times { Agent.create! } 65 | expect([User.count, Agent.count]).to eq [2,2] 66 | end 67 | expect([User.count, Agent.count]).to eq [0,0] 68 | end 69 | end 70 | 71 | context "with fixtures" do 72 | it "should not clean fixtures" do 73 | 2.times { User.create! } 74 | strategy.cleaning do 75 | 2.times { Agent.create! } 76 | expect([User.count, Agent.count]).to eq [2,2] 77 | end 78 | expect([User.count, Agent.count]).to eq [2,0] 79 | end 80 | end 81 | 82 | context "without an initial start" do 83 | it "does nothing" do 84 | 2.times { User.create! } 85 | 2.times { Agent.create! } 86 | expect { strategy.cleaning {} } 87 | .to_not change { [User.count, Agent.count] } 88 | .from([2,2]) 89 | end 90 | end 91 | end 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/database_cleaner/active_record/truncation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'support/database_helper' 2 | require 'database_cleaner/active_record/truncation' 3 | 4 | RSpec.describe DatabaseCleaner::ActiveRecord::Truncation do 5 | subject(:strategy) { described_class.new } 6 | 7 | DatabaseHelper.with_all_dbs do |helper| 8 | context "using a #{helper.db} connection" do 9 | around do |example| 10 | helper.setup 11 | example.run 12 | helper.teardown 13 | end 14 | 15 | let(:connection) { helper.connection } 16 | 17 | before do 18 | allow(strategy.send(:connection)).to receive(:disable_referential_integrity).and_yield 19 | allow(strategy.send(:connection)).to receive(:database_cleaner_view_cache).and_return([]) 20 | end 21 | 22 | describe '#clean' do 23 | before do 24 | # Clean should not try to truncate database views. If it does, it will raise an error 25 | connection.execute "CREATE VIEW view1 AS SELECT * FROM schema_migrations;" 26 | end 27 | 28 | after do 29 | connection.execute "DROP VIEW view1" 30 | end 31 | 32 | context "with records" do 33 | before do 34 | 2.times { User.create! } 35 | 2.times { Agent.create! } 36 | UserProfile.create!(user_id: User.first.id) if helper.db == :postgres 37 | end 38 | 39 | it "should truncate all tables" do 40 | expect { strategy.clean } 41 | .to change { [User.count, Agent.count] } 42 | .from([2,2]) 43 | .to([0,0]) 44 | end 45 | 46 | if helper.db == :postgres 47 | it "should raise exception when trying to truncate table referenced in a foreign key constraint" do 48 | expect { described_class.new(except: ['user_profiles']).clean } 49 | .to raise_error(ActiveRecord::StatementInvalid, /cannot truncate a table referenced in a foreign key constraint/) 50 | end 51 | end 52 | 53 | it "should reset AUTO_INCREMENT index of table" do 54 | strategy.clean 55 | expect(User.create.id).to eq 1 56 | end 57 | 58 | it "should truncate all tables except for schema_migrations" do 59 | strategy.clean 60 | count = connection.select_value("select count(*) from schema_migrations;").to_i 61 | expect(count).to eq 2 62 | end 63 | 64 | it "should only truncate the tables specified in the :only option when provided" do 65 | expect { described_class.new(only: ['agents']).clean } 66 | .to change { [User.count, Agent.count] } 67 | .from([2,2]) 68 | .to([2,0]) 69 | end 70 | 71 | it "should not truncate the tables specified in the :except option" do 72 | expect { described_class.new(except: ['users']).clean } 73 | .to change { [User.count, Agent.count] } 74 | .from([2,2]) 75 | .to([2,0]) 76 | end 77 | 78 | it "should raise an error when invalid options are provided" do 79 | expect { described_class.new(foo: 'bar') }.to raise_error(ArgumentError) 80 | end 81 | 82 | it "should not truncate views" do 83 | allow(strategy.send(:connection)).to receive(:database_cleaner_view_cache).and_return(["users"]) 84 | 85 | expect { strategy.clean } 86 | .to change { [User.count, Agent.count] } 87 | .from([2,2]) 88 | .to([2,0]) 89 | end 90 | end 91 | 92 | describe "with pre_count optimization option" do 93 | subject(:strategy) { described_class.new(pre_count: true) } 94 | 95 | it "only truncates non-empty tables" do 96 | User.create! 97 | 98 | expect(strategy.send(:connection)).to receive(:truncate_tables).with(['users']) 99 | strategy.clean 100 | end 101 | end 102 | 103 | describe "with truncate_option set to cascade" do 104 | subject(:strategy) { described_class.new(truncate_option: :cascade) } 105 | 106 | it "specifies cascade when truncating" do 107 | User.create!({name: 1}) 108 | 109 | expect(strategy.send(:connection)).to receive(:truncate_tables).with(['users', 'agents'], {truncate_option: :cascade}) 110 | strategy.clean 111 | end 112 | end 113 | 114 | describe "with no truncate_option set" do 115 | subject(:strategy) { described_class.new } 116 | 117 | it "specifies restrict when truncating" do 118 | User.create! 119 | 120 | expect(strategy.send(:connection)).to receive(:truncate_tables).with(['users', 'agents'], {truncate_option: :restrict}) 121 | strategy.clean 122 | end 123 | end 124 | 125 | context 'when :cache_tables is set to true' do 126 | subject(:strategy) { described_class.new(cache_tables: true) } 127 | 128 | it 'caches the list of tables to be truncated' do 129 | expect(strategy.send(:connection)).to receive(:database_cleaner_table_cache).and_return([]) 130 | expect(strategy.send(:connection)).not_to receive(:tables) 131 | 132 | allow(strategy.send(:connection)).to receive(:truncate_tables) 133 | subject.clean 134 | end 135 | end 136 | 137 | context 'when :cache_tables is set to false' do 138 | subject(:strategy) { described_class.new(cache_tables: false) } 139 | 140 | it 'does not cache the list of tables to be truncated' do 141 | expect(strategy.send(:connection)).not_to receive(:database_cleaner_table_cache) 142 | expect(strategy.send(:connection)).to receive(:database_tables).and_return([]) 143 | 144 | allow(strategy.send(:connection)).to receive(:truncate_tables) 145 | strategy.clean 146 | end 147 | end 148 | end 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/database_cleaner/active_record_spec.rb: -------------------------------------------------------------------------------- 1 | require 'database_cleaner/active_record' 2 | require 'database_cleaner/spec' 3 | 4 | RSpec.describe DatabaseCleaner::ActiveRecord do 5 | it_behaves_like "a database_cleaner adapter" 6 | 7 | describe "config_file_location" do 8 | after do 9 | # prevent global state leakage 10 | DatabaseCleaner::ActiveRecord.config_file_location = nil 11 | end 12 | 13 | it "should default to \#{Dir.pwd}/config/database.yml" do 14 | DatabaseCleaner::ActiveRecord.config_file_location = nil 15 | expect(DatabaseCleaner::ActiveRecord.config_file_location).to \ 16 | eq "#{Dir.pwd}/config/database.yml" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | 3 | if ENV['COVERAGE'] == 'true' 4 | require "simplecov" 5 | 6 | if ENV['CI'] == 'true' 7 | require 'codecov' 8 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 9 | puts "required codecov" 10 | end 11 | 12 | # ensure all test run coverage results are merged 13 | command_name = File.basename(ENV["BUNDLE_GEMFILE"]) 14 | SimpleCov.command_name command_name 15 | SimpleCov.start 16 | puts "started simplecov: #{command_name}" 17 | end 18 | 19 | require 'database_cleaner-active_record' 20 | 21 | ActiveRecord::Base # load active record 22 | 23 | RSpec.configure do |config| 24 | # These two settings work together to allow you to limit a spec run 25 | # to individual examples or groups you care about by tagging them with 26 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 27 | # get run. 28 | config.filter_run :focus 29 | config.run_all_when_everything_filtered = true 30 | 31 | config.disable_monkey_patching! 32 | end 33 | -------------------------------------------------------------------------------- /spec/support/aliased-example.database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | encoding: utf8 3 | reconnect: true 4 | 5 | my_db: 6 | <<: *default 7 | database: <%= "ONE".downcase %> 8 | 9 | other_db: 10 | <<: *default 11 | database: <%= "TWO".downcase %> 12 | -------------------------------------------------------------------------------- /spec/support/database_helper.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'database_cleaner/spec/database_helper' 3 | 4 | class DatabaseHelper < DatabaseCleaner::Spec::DatabaseHelper 5 | def self.with_all_dbs &block 6 | all_dbs = %w[mysql2 sqlite3 postgres] 7 | all_dbs << :trilogy if Gem::Version.new(ActiveRecord::VERSION::STRING) >= Gem::Version.new("7.1.0") 8 | all_dbs.map(&:to_sym).each do |db| 9 | yield new(db) 10 | end 11 | end 12 | 13 | def setup 14 | Kernel.const_set "User", Class.new(ActiveRecord::Base) 15 | Kernel.const_set "Agent", Class.new(ActiveRecord::Base) 16 | Kernel.const_set "UserProfile", Class.new(ActiveRecord::Base) if db == :postgres 17 | 18 | super 19 | 20 | connection.execute "CREATE TABLE IF NOT EXISTS schema_migrations (version VARCHAR(255));" 21 | connection.execute "INSERT INTO schema_migrations VALUES (1), (2);" 22 | end 23 | 24 | def teardown 25 | connection.execute "DROP TABLE schema_migrations;" 26 | 27 | super 28 | 29 | Kernel.send :remove_const, "User" if defined?(User) 30 | Kernel.send :remove_const, "Agent" if defined?(Agent) 31 | Kernel.send :remove_const, "UserProfile" if defined?(UserProfile) 32 | end 33 | 34 | private 35 | 36 | def establish_connection(config = default_config) 37 | ActiveRecord::Base.establish_connection(config) 38 | @connection = ActiveRecord::Base.connection 39 | end 40 | 41 | def load_schema 42 | id_column = case db 43 | when :sqlite3 44 | "id INTEGER PRIMARY KEY AUTOINCREMENT" 45 | when :mysql2, :trilogy 46 | "id INTEGER PRIMARY KEY AUTO_INCREMENT" 47 | when :postgres 48 | "id SERIAL PRIMARY KEY" 49 | end 50 | connection.execute <<-SQL 51 | CREATE TABLE IF NOT EXISTS users ( 52 | #{id_column}, 53 | name INTEGER 54 | ); 55 | SQL 56 | 57 | connection.execute <<-SQL 58 | CREATE TABLE IF NOT EXISTS agents ( 59 | name INTEGER 60 | ); 61 | SQL 62 | 63 | if db == :postgres 64 | connection.execute <<-SQL 65 | CREATE TABLE IF NOT EXISTS user_profiles ( 66 | user_id INTEGER, 67 | FOREIGN KEY(user_id) REFERENCES users(id) 68 | ); 69 | SQL 70 | end 71 | end 72 | 73 | def drop_db 74 | if db == :postgres 75 | connection.execute "DROP TABLE IF EXISTS user_profiles" 76 | end 77 | 78 | super 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/support/example.database.yml: -------------------------------------------------------------------------------- 1 | my_db: 2 | database: <%= "ONE".downcase %> 3 | -------------------------------------------------------------------------------- /spec/support/sample.config.yml: -------------------------------------------------------------------------------- 1 | mysql2: 2 | adapter: mysql2 3 | database: database_cleaner_test 4 | username: root 5 | password: 6 | host: 127.0.0.1 7 | port: 3306 8 | encoding: utf8 9 | 10 | trilogy: 11 | adapter: trilogy 12 | database: database_cleaner_test 13 | username: root 14 | password: 15 | host: 127.0.0.1 16 | port: 3306 17 | encoding: utf8 18 | 19 | postgres: 20 | adapter: postgresql 21 | database: database_cleaner_test 22 | username: postgres 23 | password: postgres 24 | host: 127.0.0.1 25 | encoding: unicode 26 | template: template0 27 | 28 | sqlite3: 29 | adapter: sqlite3 30 | database: tmp/database_cleaner_test.sqlite3 31 | pool: 5 32 | timeout: 5000 33 | encoding: utf8 34 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DatabaseCleaner/database_cleaner-active_record/912acbe1facddfe1b14f21107f6ddc9df290de53/tmp/.keep --------------------------------------------------------------------------------