Fast truncation strategy for ActiveRecord with mysql, mysql2 or pg

This commit is contained in:
stanislaw 2012-07-08 16:23:37 +03:00 committed by Ben Mabey
parent 8507ec04e1
commit e2b5086d47
11 changed files with 588 additions and 48 deletions

View File

@ -52,12 +52,86 @@ module ActiveRecord
def truncate_table(table_name)
execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
end
def fast_truncate_tables *tables_and_opts
opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
reset_ids = opts[:reset_ids] != false
_tables = tables_and_opts
_tables.each do |table_name|
if reset_ids
truncate_table_with_id_reset(table_name)
else
truncate_table_no_id_reset(table_name)
end
end
end
def truncate_table_with_id_reset(table_name)
rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").fetch_row.first.to_i
if rows_exist == 0
auto_inc = execute(<<-AUTO_INCREMENT
SELECT Auto_increment
FROM information_schema.tables
WHERE table_name='#{table_name}';
AUTO_INCREMENT
)
truncate_table(table_name) if auto_inc.fetch_row.first.to_i > 1
else
truncate_table(table_name)
end
end
def truncate_table_no_id_reset(table_name)
rows_exist = execute("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").fetch_row.first.to_i
truncate_table(table_name) if rows_exist > 0
end
end
class Mysql2Adapter < MYSQL2_ADAPTER_PARENT
def truncate_table(table_name)
execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
end
def fast_truncate_tables *tables_and_opts
opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
reset_ids = opts[:reset_ids] != false
_tables = tables_and_opts
_tables.each do |table_name|
if reset_ids
truncate_table_with_id_reset(table_name)
else
truncate_table_no_id_reset(table_name)
end
end
end
def truncate_table_with_id_reset(table_name)
rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").first.first.to_i
if rows_exist == 0
auto_inc = execute(<<-AUTO_INCREMENT
SELECT Auto_increment
FROM information_schema.tables
WHERE table_name='#{table_name}';
AUTO_INCREMENT
)
truncate_table(table_name) if auto_inc.first.first.to_i > 1
else
truncate_table(table_name)
end
end
def truncate_table_no_id_reset(table_name)
rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").first.first
truncate_table(table_name) if rows_exist == 1
end
end
class IBM_DBAdapter < AbstractAdapter
@ -101,12 +175,60 @@ module ActiveRecord
def truncate_table(table_name)
truncate_tables([table_name])
end
def truncate_tables(table_names)
return if table_names.nil? || table_names.empty?
execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}.join(', ')} #{restart_identity} #{cascade};")
end
def fast_truncate_tables(*tables_and_opts)
opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
reset_ids = opts[:reset_ids] != false
_tables = tables_and_opts
if reset_ids
truncate_tables_with_id_reset(_tables)
else
truncate_tables_no_id_reset(_tables)
end
end
def truncate_tables_with_id_reset(_tables)
tables_to_truncate = []
_tables.each do |table|
begin
table_curr_value = execute(<<-CURR_VAL
SELECT currval('#{table}_id_seq');
CURR_VAL
).first['currval'].to_i
rescue ActiveRecord::StatementInvalid
table_curr_value = nil
end
if table_curr_value && table_curr_value > 0
tables_to_truncate << table
end
end
truncate_tables(tables_to_truncate) if tables_to_truncate.any?
end
def truncate_tables_no_id_reset(_tables)
tables_to_truncate = []
_tables.each do |table|
rows_exist = execute(<<-TR
SELECT true FROM #{table} LIMIT 1;
TR
)
tables_to_truncate << table if rows_exist.any?
end
truncate_tables(tables_to_truncate) if tables_to_truncate.any?
end
end
class SQLServerAdapter < AbstractAdapter
@ -137,7 +259,11 @@ module DatabaseCleaner::ActiveRecord
def clean
connection = connection_klass.connection
connection.disable_referential_integrity do
connection.truncate_tables(tables_to_truncate(connection))
if connection.respond_to?(:fast_truncate_tables)
connection.fast_truncate_tables(tables_to_truncate(connection), {:reset_ids => reset_ids?})
else
connection.truncate_tables(tables_to_truncate(connection))
end
end
end
@ -152,8 +278,8 @@ module DatabaseCleaner::ActiveRecord
'schema_migrations'
end
def reset_ids?
@reset_ids != false
end
end
end

View File

@ -2,8 +2,8 @@ module DatabaseCleaner
module Generic
module Truncation
def initialize(opts={})
if !opts.empty? && !(opts.keys - [:only, :except]).empty?
raise ArgumentError, "The only valid options are :only and :except. You specified #{opts.keys.join(',')}."
if !opts.empty? && !(opts.keys - [:only, :except, :reset_ids]).empty?
raise ArgumentError, "The only valid options are :only, :except, or :reset_ids. You specified #{opts.keys.join(',')}."
end
if opts.has_key?(:only) && opts.has_key?(:except)
raise ArgumentError, "You may only specify either :only or :except. Doing both doesn't really make sense does it?"
@ -12,6 +12,7 @@ module DatabaseCleaner
@only = opts[:only]
@tables_to_exclude = (opts[:except] || []).dup
@tables_to_exclude << migration_storage_name if migration_storage_name
@reset_ids = opts[:reset_ids]
end
def start

View File

@ -0,0 +1,40 @@
require 'spec_helper'
require 'active_record'
require 'support/active_record/mysql2_setup'
require 'database_cleaner/active_record/truncation'
require 'database_cleaner/active_record/truncation/shared_mysql'
module ActiveRecord
module ConnectionAdapters
describe do
before(:all) { active_record_mysql2_setup }
let(:adapter) { Mysql2Adapter }
let(:connection) { active_record_mysql2_connection }
describe "#truncate_table" do
it "should truncate the table" do
2.times { User.create }
connection.truncate_table('users')
User.count.should == 0
end
it "should reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_table('users')
User.create.id.should == 1
end
end
it_behaves_like "Fast truncation" do
let(:adapter) { Mysql2Adapter }
let(:connection) { active_record_mysql2_connection }
end
end
end
end

View File

@ -0,0 +1,40 @@
require 'spec_helper'
require 'active_record'
require 'support/active_record/mysql_setup'
require 'database_cleaner/active_record/truncation'
require 'database_cleaner/active_record/truncation/shared_mysql'
module ActiveRecord
module ConnectionAdapters
describe do
before(:all) { active_record_mysql_setup }
let(:adapter) { MysqlAdapter }
let(:connection) { active_record_mysql_connection }
describe "#truncate_table" do
it "should truncate the table" do
2.times { User.create }
connection.truncate_table('users')
User.count.should == 0
end
it "should reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_table('users')
User.create.id.should == 1
end
end
it_behaves_like "Fast truncation" do
let(:adapter) { MysqlAdapter }
let(:connection) { active_record_mysql_connection }
end
end
end
end

View File

@ -0,0 +1,103 @@
require 'spec_helper'
require 'active_record'
require 'support/active_record/postgresql_setup'
require 'database_cleaner/active_record/truncation'
module ActiveRecord
module ConnectionAdapters
describe do
before(:all) { active_record_pg_setup }
let(:adapter) { PostgreSQLAdapter }
let(:connection) do
active_record_pg_connection
end
before(:each) do
connection.truncate_tables connection.tables
end
describe "#truncate_table" do
it "should truncate the table" do
2.times { User.create }
connection.truncate_table('users')
User.count.should == 0
end
it "should reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_table('users')
User.create.id.should == 1
end
end
describe "#truncate_tables_with_id_reset" do
it "responds" do
adapter.instance_methods.should include('truncate_tables_with_id_reset')
end
it "should truncate the table" do
2.times { User.create }
connection.truncate_tables_with_id_reset('users')
User.count.should == 0
end
it "should reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_tables_with_id_reset('users')
User.create.id.should == 1
end
end
describe "#truncate_tables_no_id_reset" do
it "responds" do
adapter.instance_methods.map(&:to_s).should include('truncate_tables_no_id_reset')
end
it "should truncate the table" do
2.times { User.create }
connection.truncate_tables_no_id_reset('users')
User.count.should == 0
end
it "should not reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_tables_no_id_reset('users')
User.create.id.should == 3
end
end
describe "#fast_truncate_tables" do
it "responds" do
adapter.instance_methods.should include('fast_truncate_tables')
end
it 'should call #truncate_tables_with_id_reset on each table if :reset_ids option true was given' do
connection.should_receive(:truncate_tables_with_id_reset).exactly(connection.tables.size).times
connection.fast_truncate_tables(connection.tables)
end
it 'should call #truncate_tables_with_id_reset on each table if :reset_ids option false was given' do
connection.should_receive(:truncate_tables_no_id_reset).exactly(connection.tables.size).times
connection.fast_truncate_tables(connection.tables, :reset_ids => false)
end
end
end
end
end

View File

@ -0,0 +1,63 @@
shared_examples_for "Fast truncation" do
describe "#truncate_table_with_id_reset" do
it "responds" do
adapter.instance_methods.should include('truncate_table_with_id_reset')
end
it "should truncate the table" do
2.times { User.create }
connection.truncate_table_with_id_reset('users')
User.count.should == 0
end
it "should reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_table_with_id_reset('users')
User.create.id.should == 1
end
end
describe "#truncate_table_no_id_reset" do
it "responds" do
adapter.instance_methods.map(&:to_s).should include('truncate_table_no_id_reset')
end
it "should truncate the table" do
2.times { User.create }
connection.truncate_table_no_id_reset('users')
User.count.should == 0
end
it "should not reset AUTO_INCREMENT index of table" do
2.times { User.create }
User.delete_all
connection.truncate_table_no_id_reset('users')
User.create.id.should == 3
end
end
describe "#fast_truncate_tables" do
it "responds" do
adapter.instance_methods.should include('fast_truncate_tables')
end
it 'should call #truncate_table_with_id_reset on each table if :reset_ids option true was given' do
connection.should_receive(:truncate_table_with_id_reset).exactly(connection.tables.size).times
connection.fast_truncate_tables(connection.tables)
end
it 'should call #truncate_table_with_id_reset on each table if :reset_ids option false was given' do
connection.should_receive(:truncate_table_no_id_reset).exactly(connection.tables.size).times
connection.fast_truncate_tables(connection.tables, :reset_ids => false)
end
end
end

View File

@ -1,16 +1,15 @@
require File.dirname(__FILE__) + '/../../spec_helper'
require 'active_record'
require 'database_cleaner/active_record/truncation'
require 'database_cleaner/active_record/truncation'
module ActiveRecord
module ConnectionAdapters
[MysqlAdapter, Mysql2Adapter, SQLite3Adapter, JdbcAdapter, PostgreSQLAdapter, IBM_DBAdapter].each do |adapter|
describe adapter, "#truncate_table" do
it "responds" do
adapter.new("foo").should respond_to(:truncate_table)
adapter.instance_methods.should include('truncate_table')
end
it "should truncate the table"
end
end
end
@ -22,55 +21,81 @@ module DatabaseCleaner
describe Truncation do
let(:connection) { mock('connection') }
before(:each) do
connection.stub!(:disable_referential_integrity).and_yield
connection.stub!(:database_cleaner_view_cache).and_return([])
::ActiveRecord::Base.stub!(:connection).and_return(connection)
end
it "should truncate all tables except for schema_migrations" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
connection.should_receive(:truncate_tables).with(['widgets', 'dogs'])
Truncation.new.clean
describe '#clean' do
it "should truncate all tables except for schema_migrations" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
connection.should_receive(:truncate_tables).with(['widgets', 'dogs'])
Truncation.new.clean
end
it "should only truncate the tables specified in the :only option when provided" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
connection.should_receive(:truncate_tables).with(['widgets'])
Truncation.new(:only => ['widgets']).clean
end
it "should not truncate the tables specified in the :except option" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
connection.should_receive(:truncate_tables).with(['dogs'])
Truncation.new(:except => ['widgets']).clean
end
it "should raise an error when :only and :except options are used" do
running {
Truncation.new(:except => ['widgets'], :only => ['widgets'])
}.should raise_error(ArgumentError)
end
it "should raise an error when invalid options are provided" do
running { Truncation.new(:foo => 'bar') }.should raise_error(ArgumentError)
end
it "should not truncate views" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[widgets dogs])
connection.stub!(:database_cleaner_view_cache).and_return(["widgets"])
connection.should_receive(:truncate_tables).with(['dogs'])
Truncation.new.clean
end
describe "relying on #fast_truncate_tables if connection allows it" do
it "should rely on #fast_truncate_tables if connection allows it" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[widgets dogs])
connection.stub!(:database_cleaner_view_cache).and_return(["widgets"])
connection.should_receive(:fast_truncate_tables).with(['dogs'], :reset_ids => true)
Truncation.new.clean
end
end
end
it "should only truncate the tables specified in the :only option when provided" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
describe '#reset_ids?' do
subject { Truncation.new }
its(:reset_ids?) { should == true }
connection.should_receive(:truncate_tables).with(['widgets'])
it 'should return true if @reset_id is set and non false or nil' do
subject.instance_variable_set(:"@reset_ids", 'Something')
subject.send(:reset_ids?).should == true
end
Truncation.new(:only => ['widgets']).clean
it 'should return false if @reset_id is set to false' do
subject.instance_variable_set(:"@reset_ids", false)
subject.send(:reset_ids?).should == false
end
end
it "should not truncate the tables specified in the :except option" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[schema_migrations widgets dogs])
connection.should_receive(:truncate_tables).with(['dogs'])
Truncation.new(:except => ['widgets']).clean
end
it "should raise an error when :only and :except options are used" do
running {
Truncation.new(:except => ['widgets'], :only => ['widgets'])
}.should raise_error(ArgumentError)
end
it "should raise an error when invalid options are provided" do
running { Truncation.new(:foo => 'bar') }.should raise_error(ArgumentError)
end
it "should not truncate views" do
connection.stub!(:database_cleaner_table_cache).and_return(%w[widgets dogs])
connection.stub!(:database_cleaner_view_cache).and_return(["widgets"])
connection.should_receive(:truncate_tables).with(['dogs'])
Truncation.new.clean
end
end
end
end

View File

@ -13,6 +13,10 @@ module ::DatabaseCleaner
def except
@tables_to_exclude
end
def reset_ids?
!!@reset_ids
end
end
class MigrationExample < TruncationExample
@ -44,6 +48,7 @@ module ::DatabaseCleaner
it { expect{ TruncationExample.new( { :except => "something",:only => "something else" } ) }.to raise_error(ArgumentError) }
it { expect{ TruncationExample.new( { :only => "something" } ) }.to_not raise_error(ArgumentError) }
it { expect{ TruncationExample.new( { :except => "something" } ) }.to_not raise_error(ArgumentError) }
it { expect{ TruncationExample.new( { :reset_ids => "something" } ) }.to_not raise_error(ArgumentError) }
context "" do
subject { TruncationExample.new( { :only => ["something"] } ) }
@ -57,6 +62,16 @@ module ::DatabaseCleaner
its(:except) { should include("something") }
end
context "" do
subject { TruncationExample.new( { :reset_ids => ["something"] } ) }
its(:reset_ids?) { should == true }
end
context "" do
subject { TruncationExample.new( { :reset_ids => nil } ) }
its(:reset_ids?) { should == false }
end
context "" do
subject { MigrationExample.new }
its(:only) { should == nil }

View File

@ -0,0 +1,41 @@
module MySQL2Helper
puts "Active Record #{ActiveRecord::VERSION::STRING}, mysql2"
# ActiveRecord::Base.logger = Logger.new(STDERR)
@@mysql2_db_spec = {
:adapter => 'mysql2',
:host => 'localhost',
:username => 'root',
:password => '',
:encoding => 'utf8'
}
@@db = {:database => 'database_cleaner_test'}
def active_record_mysql2_setup
ActiveRecord::Base.establish_connection(@@mysql2_db_spec)
ActiveRecord::Base.connection.drop_database @@db[:database] rescue nil
ActiveRecord::Base.connection.create_database @@db[:database]
ActiveRecord::Base.establish_connection(@@mysql2_db_spec.merge(@@db))
ActiveRecord::Schema.define do
create_table :users, :force => true do |t|
t.integer :name
end
end
end
def active_record_mysql2_connection
ActiveRecord::Base.connection
end
class ::User < ActiveRecord::Base
end
end
RSpec.configure do |c|
c.include MySQL2Helper
end

View File

@ -0,0 +1,41 @@
module MySQLHelper
puts "Active Record #{ActiveRecord::VERSION::STRING}, mysql"
# ActiveRecord::Base.logger = Logger.new(STDERR)
@@mysql_db_spec = {
:adapter => 'mysql',
:host => 'localhost',
:username => 'root',
:password => '',
:encoding => 'utf8'
}
@@db = {:database => 'database_cleaner_test'}
def active_record_mysql_setup
ActiveRecord::Base.establish_connection(@@mysql_db_spec)
ActiveRecord::Base.connection.drop_database @@db[:database] rescue nil
ActiveRecord::Base.connection.create_database @@db[:database]
ActiveRecord::Base.establish_connection(@@mysql_db_spec.merge(@@db))
ActiveRecord::Schema.define do
create_table :users, :force => true do |t|
t.integer :name
end
end
end
def active_record_mysql_connection
ActiveRecord::Base.connection
end
class ::User < ActiveRecord::Base
end
end
RSpec.configure do |c|
c.include MySQLHelper
end

View File

@ -0,0 +1,45 @@
module PostgreSQLHelper
puts "Active Record #{ActiveRecord::VERSION::STRING}, pg"
# ActiveRecord::Base.logger = Logger.new(STDERR)
# createdb database_cleaner_test -E UTF8 -T template0
@@pg_db_spec = {
:adapter => 'postgresql',
:database => 'postgres',
:host => '127.0.0.1',
:username => 'postgres',
:password => '',
:encoding => 'unicode',
:template => 'template0'
}
@@db = {:database => 'database_cleaner_test'}
# ActiveRecord::Base.establish_connection(@@pg_db_spec)
# ActiveRecord::Base.connection.drop_database db[:database] rescue nil
# ActiveRecord::Base.connection.create_database db[:database]
def active_record_pg_setup
ActiveRecord::Base.establish_connection(@@pg_db_spec.merge(@@db))
ActiveRecord::Schema.define do
create_table :users, :force => true do |t|
t.integer :name
end
end
end
def active_record_pg_connection
ActiveRecord::Base.connection
end
class ::User < ActiveRecord::Base
end
end
RSpec.configure do |c|
c.include PostgreSQLHelper
end