├── .gitignore ├── .rspec ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── database_flusher.gemspec ├── lib ├── database_flusher.rb └── database_flusher │ ├── active_record │ ├── adapters │ │ ├── abstract_adapter.rb │ │ ├── mysql2_adapter.rb │ │ ├── postgresql_adapter.rb │ │ └── sqlite_adapter.rb │ ├── deletion_strategy.rb │ └── transaction_strategy.rb │ ├── cleaner.rb │ ├── mongoid │ └── deletion_strategy.rb │ ├── null_strategy.rb │ └── version.rb └── spec ├── database.yml ├── database_flusher ├── active_record │ ├── deletion_strategy_spec.rb │ └── transaction_strategy_spec.rb ├── cleaner_spec.rb ├── mongoid │ └── deletion_strategy_spec.rb └── version_spec.rb ├── database_flusher_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /*.gem 11 | /.byebug_history 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | cache: bundler 4 | 5 | services: 6 | - mongodb 7 | 8 | rvm: 9 | - 2.2.4 10 | - 2.3.3 11 | 12 | env: 13 | - DB=sqlite3 14 | - DB=mysql2 15 | - DB=postgresql 16 | 17 | before_install: gem install bundler -v 1.13.3 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at edgars.beigarts@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in database_flusher.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Edgars Beigarts 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 | # DatabaseFlusher 2 | 3 | [![Build Status](https://travis-ci.org/ebeigarts/database_flusher.svg?branch=master)](https://travis-ci.org/ebeigarts/database_flusher) 4 | [![Code Climate](https://codeclimate.com/github/ebeigarts/database_flusher/badges/gpa.svg)](https://codeclimate.com/github/ebeigarts/database_flusher) 5 | 6 | database_flusher is a tiny and fast database cleaner inspired by [database_cleaner](https://github.com/DatabaseCleaner/database_cleaner) and [database_rewinder](https://github.com/amatsuda/database_rewinder). 7 | 8 | ## Features 9 | 10 | * No monkey patching - uses `ActiveSupport::Notifications` and `Mongo::Monitoring` to catch `INSERT` statements 11 | * Fast `:deletion` strategy that cleans only tables/collections where `INSERT` statements were performed 12 | * Faster `disable_referential_integrity` for PostgreSQL 13 | * Executes multiple `DELETE` statements as one query with ActiveRecord 14 | * Supports only one database for each ORM 15 | 16 | ## Supported ORMs and strategies 17 | 18 | | ORM | Deletion | Transaction | 19 | |:--------------------|:---------|:------------| 20 | | ActiveRecord >= 4.2 | Yes | Yes | 21 | | Mongoid >= 5.0 | Yes | No | 22 | 23 | mysql2 needs `MULTI_STATEMENTS` flag to be set and requires active_record >= 5.0.0 24 | 25 | ```yaml 26 | adapter: mysql2 27 | flags: 28 | - MULTI_STATEMENTS 29 | ``` 30 | 31 | ## Installation 32 | 33 | Add this line to your application's Gemfile: 34 | 35 | ```ruby 36 | gem 'database_flusher' 37 | ``` 38 | 39 | And then execute: 40 | 41 | ```bash 42 | $ bundle 43 | ``` 44 | 45 | ## Usage 46 | 47 | ### RSpec 48 | 49 | ```ruby 50 | RSpec.configure do |config| 51 | config.use_transactional_fixtures = false 52 | 53 | config.before :suite do 54 | if File.exists?('.~lock') 55 | puts "Unclean shutdown, cleaning the whole database..." 56 | DatabaseFlusher[:active_record].clean_with(:deletion) 57 | DatabaseFlusher[:mongoid].clean_with(:deletion) 58 | else 59 | File.open('.~lock', 'a') {} 60 | end 61 | 62 | DatabaseFlusher[:active_record].strategy = :transaction 63 | DatabaseFlusher[:mongoid].strategy = :deletion 64 | end 65 | 66 | config.after :suite do 67 | File.unlink('.~lock') 68 | end 69 | 70 | config.before :each do 71 | DatabaseFlusher[:active_record].strategy = :transaction 72 | end 73 | 74 | config.before :each, type: :feature do 75 | if Capybara.current_driver != :rack_test 76 | DatabaseFlusher[:active_record].strategy = :deletion 77 | end 78 | end 79 | 80 | config.before :each do 81 | DatabaseFlusher.start 82 | end 83 | 84 | config.append_after :each do 85 | DatabaseFlusher.clean 86 | end 87 | end 88 | ``` 89 | 90 | ### Cucumber 91 | 92 | ```ruby 93 | if File.exists?('.~lock') 94 | puts "Unclean shutdown, cleaning the whole database..." 95 | DatabaseFlusher[:active_record].clean_with(:deletion) 96 | DatabaseFlusher[:mongoid].clean_with(:deletion) 97 | else 98 | File.open('.~lock', 'a') {} 99 | end 100 | 101 | at_exit do 102 | File.unlink('.~lock') 103 | end 104 | 105 | DatabaseFlusher[:active_record].strategy = :transaction 106 | DatabaseFlusher[:mongoid].strategy = :deletion 107 | 108 | # Use Before hook to make sure it runs after capybara driver is set. 109 | Before do 110 | if Capybara.current_driver == :rack_test 111 | DatabaseFlusher[:active_record].strategy = :transaction 112 | else 113 | DatabaseFlusher[:active_record].strategy = :deletion 114 | end 115 | DatabaseFlusher.start 116 | end 117 | 118 | # Use Around hook to make sure it runs after capybara session reset. 119 | Around do |scenario, block| 120 | begin 121 | block.call 122 | ensure 123 | DatabaseFlusher.clean 124 | end 125 | end 126 | ``` 127 | 128 | ## Contributing 129 | 130 | Bug reports and pull requests are welcome on GitHub at https://github.com/ebeigarts/database_flusher. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 131 | 132 | ## License 133 | 134 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 135 | -------------------------------------------------------------------------------- /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_flusher" 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 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /database_flusher.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'database_flusher/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'database_flusher' 8 | spec.version = DatabaseFlusher::VERSION 9 | spec.authors = ['Edgars Beigarts'] 10 | spec.email = ['edgars.beigarts@gmail.com'] 11 | 12 | spec.summary = %q{super-fast database cleaner} 13 | spec.description = spec.summary 14 | spec.homepage = 'https://github.com/ebeigarts/database_flusher' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.require_paths = ['lib'] 21 | 22 | spec.required_ruby_version = '>= 2.2.1' 23 | 24 | spec.add_development_dependency 'bundler', '~> 1.13' 25 | spec.add_development_dependency 'rake', '~> 10.0' 26 | spec.add_development_dependency 'rspec', '~> 3.0' 27 | spec.add_development_dependency 'byebug' 28 | 29 | spec.add_development_dependency 'activerecord' 30 | spec.add_development_dependency 'sqlite3' 31 | spec.add_development_dependency 'mysql2' 32 | spec.add_development_dependency 'pg' 33 | 34 | spec.add_development_dependency 'mongoid' 35 | end 36 | -------------------------------------------------------------------------------- /lib/database_flusher.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'set' 3 | 4 | require 'database_flusher/version' 5 | require 'database_flusher/null_strategy' 6 | require 'database_flusher/cleaner' 7 | 8 | require 'database_flusher/active_record/deletion_strategy' 9 | require 'database_flusher/active_record/transaction_strategy' 10 | require 'database_flusher/mongoid/deletion_strategy' 11 | 12 | module DatabaseFlusher 13 | extend self 14 | 15 | def cleaners 16 | @cleaners ||= {} 17 | end 18 | 19 | def [](name) 20 | cleaners[name] ||= DatabaseFlusher::Cleaner.new(name) 21 | end 22 | 23 | def start 24 | cleaners.values.each(&:start) 25 | end 26 | 27 | def stop 28 | cleaners.values.each(&:stop) 29 | end 30 | 31 | def clean 32 | cleaners.values.each(&:clean) 33 | end 34 | 35 | def cleaning 36 | start 37 | yield 38 | ensure 39 | clean 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/adapters/abstract_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module ActiveRecord 4 | class AbstractAdapter 5 | attr_reader :connection, :raw_connection 6 | 7 | def initialize(connection) 8 | @connection = connection 9 | @raw_connection = connection.raw_connection 10 | end 11 | 12 | def delete(*tables) 13 | disable_referential_integrity(*tables) do 14 | stmts = tables.map do |name| 15 | "DELETE FROM #{quote_table_name(name)}" 16 | end 17 | sql = stmts.join(';') 18 | execute_multi sql 19 | end 20 | end 21 | 22 | private 23 | 24 | def execute_multi(sql) 25 | connection.execute sql 26 | end 27 | 28 | def execute(sql) 29 | connection.execute sql 30 | end 31 | 32 | def quote_table_name(name) 33 | connection.quote_table_name(name) 34 | end 35 | 36 | def disable_referential_integrity(*tables, &block) 37 | connection.disable_referential_integrity(&block) 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/adapters/mysql2_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module ActiveRecord 4 | class Mysql2Adapter < AbstractAdapter 5 | def initialize(connection) 6 | super 7 | flags = raw_connection.query_options[:flags] 8 | unless flags.include?('MULTI_STATEMENTS'.freeze) 9 | raise 'MULTI_STATEMENTS flag is not enabled' 10 | end 11 | end 12 | 13 | private 14 | 15 | def execute_multi(sql) 16 | execute sql 17 | raw_connection.abandon_results! 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/adapters/postgresql_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module ActiveRecord 4 | class PostgreSQLAdapter < AbstractAdapter 5 | private 6 | 7 | def disable_referential_integrity(*tables) 8 | execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";")) 9 | yield 10 | ensure 11 | execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";")) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/adapters/sqlite_adapter.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module ActiveRecord 4 | class SQLiteAdapter < AbstractAdapter 5 | private 6 | 7 | def execute_multi(sql) 8 | raw_connection.execute_batch sql 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/deletion_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'database_flusher/active_record/adapters/abstract_adapter' 3 | require 'database_flusher/active_record/adapters/mysql2_adapter' 4 | require 'database_flusher/active_record/adapters/postgresql_adapter' 5 | require 'database_flusher/active_record/adapters/sqlite_adapter' 6 | 7 | module DatabaseFlusher 8 | module ActiveRecord 9 | class DeletionStrategy 10 | attr_reader :tables 11 | 12 | class Subscriber 13 | # INSERT [IGNORE] [INTO] schema_name.table_name 14 | PATTERN = %r{ 15 | \A\s* 16 | INSERT 17 | (?:\s+IGNORE)? 18 | (?:\s+INTO)? 19 | \s+ 20 | (?:[`"]?([^.\s`"]+)[`"]?\.)? # schema 21 | (?:[`"]?([^.\s`"]+)[`"]?) # table 22 | }xi 23 | 24 | def initialize(strategy) 25 | @strategy = strategy 26 | end 27 | 28 | def call(_, _, _, _, payload) 29 | sql = payload[:sql] 30 | match = sql.match(PATTERN) 31 | return unless match 32 | table = match[2] 33 | if table 34 | schema = match[1] 35 | if schema 36 | table = "#{schema}.#{table}" 37 | end 38 | @strategy.tables << table 39 | end 40 | end 41 | end 42 | 43 | def initialize 44 | @tables = Set.new 45 | end 46 | 47 | def start 48 | @subscriber ||= ActiveSupport::Notifications.subscribe( 49 | 'sql.active_record', 50 | Subscriber.new(self) 51 | ) 52 | end 53 | 54 | def stop 55 | if @subscriber 56 | ActiveSupport::Notifications.unsubscribe(@subscriber) 57 | @subscriber = nil 58 | end 59 | end 60 | 61 | def clean 62 | return if tables.empty? 63 | 64 | # puts "Cleaning #{tables.inspect}" 65 | adapter.delete(*tables) 66 | 67 | tables.clear 68 | end 69 | 70 | def clean_all 71 | adapter.delete(*all_tables) 72 | end 73 | 74 | private 75 | 76 | def connection 77 | @connection ||= ::ActiveRecord::Base.connection 78 | end 79 | 80 | def adapter 81 | @adapter ||= DatabaseFlusher::ActiveRecord. 82 | const_get("#{connection.adapter_name}Adapter"). 83 | new(connection) 84 | end 85 | 86 | def all_tables 87 | # NOTE connection.tables warns on AR 5 with some adapters 88 | tables = ActiveSupport::Deprecation.silence { connection.tables } 89 | tables.reject do |t| 90 | (t == ::ActiveRecord::SchemaMigration.table_name) || 91 | (::ActiveRecord::Base.respond_to?(:internal_metadata_table_name) && 92 | (t == ::ActiveRecord::Base.internal_metadata_table_name)) 93 | end 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/database_flusher/active_record/transaction_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module ActiveRecord 4 | class TransactionStrategy 5 | def start 6 | # Hack to make sure that the connection is properly setup for 7 | # the clean code. 8 | ::ActiveRecord::Base.connection.transaction{ } 9 | 10 | ::ActiveRecord::Base.connection.begin_transaction joinable: false 11 | end 12 | 13 | def stop 14 | ::ActiveRecord::Base.connection_pool.connections.each do |connection| 15 | next unless connection.open_transactions > 0 16 | connection.rollback_transaction 17 | end 18 | end 19 | 20 | def clean 21 | stop 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/database_flusher/cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | class Cleaner 4 | attr_reader :strategy 5 | 6 | def initialize(orm) 7 | @orm = orm 8 | reset_strategy 9 | end 10 | 11 | def strategy=(name) 12 | strategy_changed = name != @strategy_name 13 | 14 | stop if strategy_changed 15 | 16 | if name 17 | create_strategy(name) if strategy_changed 18 | else 19 | reset_strategy 20 | end 21 | end 22 | 23 | def clean_with(name) 24 | self.strategy = name 25 | strategy.clean_all 26 | end 27 | 28 | def start 29 | strategy.start 30 | end 31 | 32 | def stop 33 | strategy.stop 34 | end 35 | 36 | def clean 37 | strategy.clean 38 | end 39 | 40 | private 41 | 42 | def create_strategy(name) 43 | @strategy_name = name 44 | @strategy = DatabaseFlusher. 45 | const_get(classify(@orm)). 46 | const_get("#{classify(name)}Strategy").new 47 | end 48 | 49 | def reset_strategy 50 | @strategy_name = nil 51 | @strategy = DatabaseFlusher::NullStrategy.new 52 | end 53 | 54 | def classify(name) 55 | name.to_s.split('_').collect(&:capitalize).join 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/database_flusher/mongoid/deletion_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | module Mongoid 4 | class DeletionStrategy 5 | attr_reader :collections 6 | 7 | class Subscriber 8 | def initialize(strategy) 9 | @strategy = strategy 10 | end 11 | 12 | def started(event) 13 | collection = event.command['insert'.freeze] 14 | if collection 15 | @strategy.collections << collection 16 | end 17 | end 18 | 19 | private 20 | 21 | def method_missing(*args, &block) 22 | end 23 | end 24 | 25 | def initialize 26 | @collections = Set.new 27 | end 28 | 29 | def start 30 | @subscriber ||= client.subscribe( 31 | Mongo::Monitoring::COMMAND, 32 | Subscriber.new(self) 33 | ) 34 | end 35 | 36 | def stop 37 | raise NotImplementedError, "Mongo doesn't provide unsubscribe" 38 | end 39 | 40 | def clean 41 | return if collections.empty? 42 | # puts "Cleaning #{collections.inspect}" 43 | collections.each do |name| 44 | client[name].delete_many 45 | end 46 | collections.clear 47 | end 48 | 49 | def clean_all 50 | all_collections.each do |name| 51 | client[name].delete_many 52 | end 53 | end 54 | 55 | private 56 | 57 | def client 58 | @client ||= ::Mongoid::Clients.default 59 | end 60 | 61 | def all_collections 62 | client.database.collections.collect { |c| c.namespace.split('.',2)[1] } 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/database_flusher/null_strategy.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | class NullStrategy 4 | def start; end 5 | def stop; end 6 | def clean; end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/database_flusher/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module DatabaseFlusher 3 | VERSION = '0.3.4'.freeze 4 | end 5 | -------------------------------------------------------------------------------- /spec/database.yml: -------------------------------------------------------------------------------- 1 | sqlite3: 2 | adapter: sqlite3 3 | database: ":memory:" 4 | 5 | mysql2: 6 | adapter: mysql2 7 | host: localhost 8 | username: root 9 | password: 10 | database: database_flusher 11 | flags: 12 | - MULTI_STATEMENTS 13 | 14 | postgresql: 15 | adapter: postgresql 16 | host: localhost 17 | username: 18 | password: 19 | database: database_flusher 20 | -------------------------------------------------------------------------------- /spec/database_flusher/active_record/deletion_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe DatabaseFlusher::ActiveRecord::DeletionStrategy do 5 | subject!(:cleaner) { described_class.new } 6 | 7 | after do 8 | ActiveRecordPost.delete_all 9 | ActiveRecordComment.delete_all 10 | end 11 | 12 | describe '#clean_all' do 13 | it 'cleans the whole database' do 14 | ActiveRecordPost.create! 15 | ActiveRecordComment.create! 16 | cleaner.clean_all 17 | expect(ActiveRecordPost.count).to eq(0) 18 | expect(ActiveRecordComment.count).to eq(0) 19 | end 20 | end 21 | 22 | describe '#clean' do 23 | it 'cleans the database' do 24 | begin 25 | cleaner.start 26 | ActiveRecordPost.create! 27 | ActiveRecordComment.create! 28 | expect(cleaner.tables.to_a).to eq(['posts', 'comments']) 29 | ensure 30 | cleaner.clean 31 | end 32 | expect(ActiveRecordPost.count).to eq(0) 33 | expect(ActiveRecordComment.count).to eq(0) 34 | 35 | cleaner.stop 36 | ActiveRecordPost.create! 37 | expect(cleaner.tables.to_a).to eq([]) 38 | cleaner.clean 39 | expect(ActiveRecordPost.count).to eq(1) 40 | end 41 | 42 | if ENV['DB'] == 'mysql2' 43 | it 'cleans the database when table name is prefixed with schema' do 44 | posts = Class.new(ActiveRecordPost) do 45 | self.table_name = 'database_flusher.posts' 46 | end 47 | begin 48 | cleaner.start 49 | posts.create! 50 | expect(cleaner.tables.to_a).to eq(['database_flusher.posts']) 51 | ensure 52 | cleaner.clean 53 | end 54 | expect(posts.count).to eq(0) 55 | end 56 | end 57 | 58 | if ENV['DB'] == 'postgresql' 59 | it 'cleans the database when table name is prefixed with schema' do 60 | posts = Class.new(ActiveRecordPost) do 61 | self.table_name = 'public.posts' 62 | end 63 | begin 64 | cleaner.start 65 | posts.create! 66 | expect(cleaner.tables.to_a).to eq(['public.posts']) 67 | ensure 68 | cleaner.clean 69 | end 70 | expect(posts.count).to eq(0) 71 | end 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/database_flusher/active_record/transaction_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe DatabaseFlusher::ActiveRecord::TransactionStrategy do 5 | subject!(:cleaner) { described_class.new } 6 | 7 | after do 8 | ActiveRecordPost.delete_all 9 | ActiveRecordComment.delete_all 10 | end 11 | 12 | describe '#clean' do 13 | it 'cleans the database' do 14 | begin 15 | cleaner.start 16 | ActiveRecordPost.create! 17 | ActiveRecordComment.create! 18 | ensure 19 | cleaner.clean 20 | end 21 | expect(ActiveRecordPost.count).to eq(0) 22 | expect(ActiveRecordComment.count).to eq(0) 23 | 24 | cleaner.stop 25 | ActiveRecordPost.create! 26 | cleaner.clean 27 | expect(ActiveRecordPost.count).to eq(1) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/database_flusher/cleaner_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | require 'active_record' 4 | 5 | describe DatabaseFlusher::Cleaner do 6 | describe '#clean_with' do 7 | it 'calls #clean_all' do 8 | DatabaseFlusher[:active_record].strategy = :deletion 9 | expect(DatabaseFlusher[:active_record].strategy).to receive(:clean_all) 10 | DatabaseFlusher[:active_record].clean_with(:deletion) 11 | end 12 | end 13 | 14 | describe '#strategy=' do 15 | it 'sets strategy' do 16 | DatabaseFlusher[:active_record].strategy = nil 17 | expect(DatabaseFlusher[:active_record].strategy).to be_kind_of( 18 | DatabaseFlusher::NullStrategy 19 | ) 20 | DatabaseFlusher[:active_record].strategy = :deletion 21 | expect(DatabaseFlusher[:active_record].strategy).to be_kind_of( 22 | DatabaseFlusher::ActiveRecord::DeletionStrategy 23 | ) 24 | DatabaseFlusher[:active_record].strategy = nil 25 | expect(DatabaseFlusher[:active_record].strategy).to be_kind_of( 26 | DatabaseFlusher::NullStrategy 27 | ) 28 | end 29 | 30 | it 'stops previous strategy' do 31 | DatabaseFlusher[:active_record].strategy = :deletion 32 | strategy = DatabaseFlusher[:active_record].strategy 33 | expect(strategy).to receive(:stop) 34 | DatabaseFlusher[:active_record].strategy = nil 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/database_flusher/mongoid/deletion_strategy_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe DatabaseFlusher::Mongoid::DeletionStrategy do 5 | subject!(:cleaner) { described_class.new } 6 | 7 | after do 8 | MongoidPost.delete_all 9 | MongoidComment.delete_all 10 | end 11 | 12 | describe '#clean_all' do 13 | it 'cleans the whole database' do 14 | MongoidPost.create! 15 | MongoidComment.create! 16 | cleaner.clean_all 17 | expect(MongoidPost.count).to eq(0) 18 | expect(MongoidComment.count).to eq(0) 19 | end 20 | end 21 | 22 | describe '#clean' do 23 | it 'cleans the database' do 24 | cleaner.start 25 | MongoidPost.create! 26 | MongoidComment.create! 27 | expect(cleaner.collections.to_a).to eq(['posts', 'comments']) 28 | cleaner.clean 29 | expect(MongoidPost.count).to eq(0) 30 | expect(MongoidComment.count).to eq(0) 31 | 32 | expect { cleaner.stop }.to raise_error(NotImplementedError) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/database_flusher/version_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe 'DatabaseFlusher::VERSION' do 5 | it 'returns version number' do 6 | expect(DatabaseFlusher::VERSION).not_to be nil 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/database_flusher_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe DatabaseFlusher do 5 | describe '#cleaning' do 6 | it 'calls #start, yields a block, and calls #clean' do 7 | expect(DatabaseFlusher).to receive(:start) 8 | expect(DatabaseFlusher).to receive(:clean) 9 | result = 0 10 | DatabaseFlusher.cleaning { result = 1 } 11 | expect(result).to eq(1) 12 | end 13 | 14 | it 'calls #clean if block yields an error' do 15 | expect(DatabaseFlusher).to receive(:start) 16 | expect(DatabaseFlusher).to receive(:clean) 17 | result = 0 18 | expect { 19 | DatabaseFlusher.cleaning { raise "Error" } 20 | }.to raise_error(RuntimeError) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | require 'database_flusher' 4 | require 'active_record' 5 | require 'active_record/tasks/database_tasks' 6 | require 'mongoid' 7 | require 'byebug' 8 | 9 | ENV['DB'] ||= 'sqlite3' 10 | ActiveRecord::Tasks::DatabaseTasks.root = File.expand_path("../", __FILE__) 11 | ActiveRecord::Base.configurations = YAML.load_file( 12 | File.expand_path('../database.yml', __FILE__) 13 | ) 14 | ActiveRecord::Tasks::DatabaseTasks.drop_current ENV['DB'] 15 | ActiveRecord::Tasks::DatabaseTasks.create_current ENV['DB'] 16 | ActiveRecord::Base.establish_connection ENV['DB'].to_sym 17 | 18 | ActiveRecord::Base.logger = Logger.new(STDOUT) 19 | ActiveRecord::Schema.define do 20 | self.verbose = false 21 | create_table :posts, force: true 22 | create_table :comments, force: true 23 | end 24 | 25 | Mongoid.configure do |config| 26 | config.connect_to 'database_flusher' 27 | end 28 | 29 | class ActiveRecordPost < ActiveRecord::Base 30 | self.table_name = 'posts' 31 | end 32 | 33 | class ActiveRecordComment < ActiveRecord::Base 34 | self.table_name = 'comments' 35 | end 36 | 37 | class MongoidPost 38 | include Mongoid::Document 39 | store_in collection: 'posts' 40 | end 41 | 42 | class MongoidComment 43 | include Mongoid::Document 44 | store_in collection: 'comments' 45 | end 46 | --------------------------------------------------------------------------------