From e7c5aa2df8f13e9e22f6a12c97fcf9afd7d1b97c Mon Sep 17 00:00:00 2001 From: Shinichi Maeshima Date: Mon, 5 Jan 2026 17:25:04 +0900 Subject: [PATCH] Enable bulk execution for `SeedDo.seed` Add a `bulk: true` option to the `SeedDo.seed` method. Normally, `SeedDo.seed` runs queries for each model, but `SeedDo.seed(bulk: true)` combines multiple queries into one. This should give a large performance improvement when a project defines a lot of seed data. Allow the number of records in each query to be set with the `batch_size` option. The default is 1000. ```ruby SeedDo.seed(bulk: { batch_size: 100 }) ``` ## Requirements Use Rails [upsert_all](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-upsert_all) internally to implement this feature. To keep the behavior close to the existing seed-do behavior, use the `:unique_by` option, so the related constraints must have unique indexes. Also, `:unique_by` is supported only by PostgreSQL and SQLite, so this feature is not available on MySQL. ## Notes `SeedDo.seed` and `SeedDo.seed(bulk: true)` do not always produce the same results. For example, with the following seed definition: ```ruby User.seed(:name) do |u| u.name = 'first' end User.create!(name: 'second') User.seed(:name) do |u| u.name = 'third' end ``` With `SeedDo.seed`, records are created in the order `first`, `second`, `third`. However, with `SeedDo.seed(bulk: true)`, the order becomes `second`, `first`, `third`. Because of this, verify the behavior carefully before using `SeedDo.seed(bulk: true)` in an existing production environment. ## Breaking Change This change also updates the interface of `SeedDo::Seeder#new`. In particular, the `quiet` and `insert_only` options can no longer be passed to `SeedDo::Seeder.new`. Since `SeedDo::Seeder` is an internal class, normal usage through `SeedDo.seed` is expected to keep working, but any code that calls `SeedDo::Seeder.new` directly will need to be updated. --- .rubocop.yml | 4 +- README.md | 24 +++++ lib/seed-do.rb | 15 ++- lib/seed-do/active_record_extension.rb | 5 +- lib/seed-do/bulk_seeder.rb | 66 +++++++++++++ lib/seed-do/runner.rb | 55 +++++++---- lib/seed-do/seeder.rb | 37 ++++--- spec/bulk_seeder_spec.rb | 75 ++++++++++++++ spec/bulk_spec.rb | 97 +++++++++++++++++++ spec/fixtures/bulk_insert.rb | 9 ++ spec/fixtures/bulk_large.rb | 7 ++ spec/fixtures/bulk_seed_once.rb | 9 ++ spec/runner_spec.rb | 56 +++++++++++ spec/spec_helper.rb | 24 +++-- spec/support/models/bulk_seeded_model.rb | 2 + spec/support/models/seeded_model.rb | 6 ++ .../models/seeded_model_no_primary_key.rb | 2 + .../models/seeded_model_no_sequence.rb | 2 + 18 files changed, 439 insertions(+), 56 deletions(-) create mode 100644 lib/seed-do/bulk_seeder.rb create mode 100644 spec/bulk_seeder_spec.rb create mode 100644 spec/bulk_spec.rb create mode 100644 spec/fixtures/bulk_insert.rb create mode 100644 spec/fixtures/bulk_large.rb create mode 100644 spec/fixtures/bulk_seed_once.rb create mode 100644 spec/support/models/bulk_seeded_model.rb create mode 100644 spec/support/models/seeded_model.rb create mode 100644 spec/support/models/seeded_model_no_primary_key.rb create mode 100644 spec/support/models/seeded_model_no_sequence.rb diff --git a/.rubocop.yml b/.rubocop.yml index 3207f1d..f9c0a25 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,4 +7,6 @@ AllCops: NewCops: enable TargetRubyVersion: 3.2 Performance: - Enabled: true \ No newline at end of file + Enabled: true +Metrics/ClassLength: + Enabled: false diff --git a/README.md b/README.md index 55a6cd8..664cc91 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,30 @@ Seed files can be run automatically using `rake db:seed_do`. There are two optio You can also do a similar thing in your code by calling `SeedDo.seed(fixture_paths, filter)`. +## Bulk upsert + +If you load a large amount of seed data, you can enable bulk upsert mode: + +```ruby +SeedDo.seed(bulk: true) +``` + +In bulk mode, SeedDo buffers seed data for each file and writes it with `upsert_all`, which can significantly reduce the number of queries. + +You can also control the batch size per `upsert_all` call. The default is `1000`. + +```ruby +SeedDo.seed(bulk: { batch_size: 100 }) +``` + +### Requirements and caveats + +- Bulk mode relies on Rails `upsert_all` +- The seed constraints are passed to `unique_by`, so the related columns must have a unique index +- Bulk mode is supported only on databases that support conflict targets, such as PostgreSQL and SQLite +- Bulk mode is not available on MySQL +- `SeedDo.seed` and `SeedDo.seed(bulk: true)` may not always produce the same record order, so verify the behavior carefully before enabling it in an existing production environment + ## Disable output To disable output from Seed Do, set `SeedDo.quiet = true`. diff --git a/lib/seed-do.rb b/lib/seed-do.rb index 623e837..335fa5b 100644 --- a/lib/seed-do.rb +++ b/lib/seed-do.rb @@ -7,6 +7,7 @@ module SeedDo autoload :Seeder, 'seed-do/seeder' autoload :ActiveRecordExtension, 'seed-do/active_record_extension' autoload :BlockHash, 'seed-do/block_hash' + autoload :BulkSeeder, 'seed-do/bulk_seeder' autoload :Runner, 'seed-do/runner' autoload :Writer, 'seed-do/writer' @@ -17,12 +18,20 @@ module SeedDo # plugin, SeedDo will set it to contain `Rails.root/db/fixtures` and # `Rails.root/db/fixtures/Rails.env` mattr_accessor :fixture_paths, default: ['db/fixtures'] - # Load seed data from files # @param [Array] fixture_paths The paths to look for seed files in # @param [Regexp] filter If given, only filenames matching this expression will be loaded - def self.seed(fixture_paths = SeedDo.fixture_paths, filter = nil) - Runner.new(fixture_paths, filter).run + # @param [Boolean] bulk If true, bulk insert/upsert will be used + def self.seed(fixture_paths = SeedDo.fixture_paths, filter = nil, bulk: false) + Runner.new(fixture_paths, filter, bulk: bulk).run + end + + def self.current_seeder + @current_seeder || Seeder.new + end + + def self.current_seeder=(seeder) + @current_seeder = seeder end end diff --git a/lib/seed-do/active_record_extension.rb b/lib/seed-do/active_record_extension.rb index e9710a7..a92738f 100644 --- a/lib/seed-do/active_record_extension.rb +++ b/lib/seed-do/active_record_extension.rb @@ -29,7 +29,7 @@ module ActiveRecordExtension # { :x => 5, :y => 9, :name => "Office" } # ) def seed(*args, &block) - SeedDo::Seeder.new(self, *parse_seed_do_args(args, block)).seed + SeedDo.current_seeder.seed(self, *parse_seed_do_args(args, block)) end # Has the same syntax as {#seed}, but if a record already exists with the same values for @@ -40,8 +40,7 @@ def seed(*args, &block) # Person.seed(:id, :id => 1, :name => "Bob") # => Name changed # Person.seed_once(:id, :id => 1, :name => "Harry") # => Name *not* changed def seed_once(*args, &block) - constraints, data = parse_seed_do_args(args, block) - SeedDo::Seeder.new(self, constraints, data, insert_only: true).seed + SeedDo.current_seeder.seed_once(self, *parse_seed_do_args(args, block)) end private diff --git a/lib/seed-do/bulk_seeder.rb b/lib/seed-do/bulk_seeder.rb new file mode 100644 index 0000000..67f82d2 --- /dev/null +++ b/lib/seed-do/bulk_seeder.rb @@ -0,0 +1,66 @@ +module SeedDo + # Buffers seeds during a file run and flushes them in bulk. + class BulkSeeder + attr_reader :buffer + + def initialize(batch_size: 1000) + @batch_size = batch_size + @buffer = nil + + validate_support! + end + + def seed(model, constraints, data) + puts " - #{model} #{data.inspect}" unless SeedDo.quiet + buffer[:seed] << { model: model, constraints: constraints, data: data } + end + + def seed_once(model, constraints, data) + puts " - #{model} #{data.inspect}" unless SeedDo.quiet + buffer[:seed_once] << { model: model, constraints: constraints, data: data } + end + + def with_seed_file + @buffer = { seed: [], seed_once: [] } + yield + process_buffer + ensure + @buffer = nil + end + + private + + def validate_support! + return if ActiveRecord::Base.connection.supports_insert_conflict_target? + + raise ArgumentError, + "Bulk mode is not supported for #{ActiveRecord::Base.connection.adapter_name}. " \ + 'The database does not support upsert operations with conflict targets. ' \ + 'Please use SeedDo.seed without the bulk option.' + end + + def process_buffer + return unless buffer + + buffer.each do |type, operations| + next if operations.empty? + + operations.chunk { |operation| [operation[:model], operation[:constraints]] } + .each do |(model, constraints), chunk| + flush_chunk(model, constraints, type, chunk) + end + end + end + + def flush_chunk(model, constraints, type, chunk) + all_data = chunk.flat_map { |operation| operation[:data] } + + options = { unique_by: constraints } + options[:on_duplicate] = :skip if type == :seed_once + + all_data.each_slice(@batch_size) do |batch| + model.upsert_all(batch, **options) + end + end + end +end diff --git a/lib/seed-do/runner.rb b/lib/seed-do/runner.rb index bac8d2d..356f42f 100644 --- a/lib/seed-do/runner.rb +++ b/lib/seed-do/runner.rb @@ -3,9 +3,7 @@ module SeedDo # Runs seed files. - # - # It is not recommended to use this class directly. Instead, use {SeedDo.seed SeedDo.seed}, which creates - # an instead of {Runner} and calls {#run #run}. + # It is not recommended to use this class directly. Instead, use {SeedDo.seed SeedDo.seed}, which creates an instead of {Runner} and calls {#run #run}. # # @see SeedDo.seed SeedDo.seed class Runner @@ -13,18 +11,31 @@ class Runner # `SeedDo.fixture_paths` if {nil}. If the argument is not an array, it will be wrapped by one. # @param [Regexp] filter If given, only seed files with a file name matching this pattern will # be used - def initialize(fixture_paths = nil, filter = nil) + # @param [Boolean, Hash] bulk If true, use upsert_all to insert/update records in bulk. + # If a hash, can include :batch_size (default: 1000) to control the number of records + # per upsert_all call. + def initialize(fixture_paths = nil, filter = nil, bulk: false) @fixture_paths = Array.wrap(fixture_paths || SeedDo.fixture_paths) @filter = filter + @seeder = build_seeder(bulk) end # Run the seed files. def run + SeedDo.current_seeder = @seeder puts "\n== Filtering seed files against regexp: #{@filter.inspect}" if @filter && !SeedDo.quiet - filenames.each do |filename| - run_file(filename) - end + filenames.each { |filename| run_file(filename) } + ensure + SeedDo.current_seeder = nil + end + + def seed(model, constraints, data) + @seeder.seed(model, constraints, data) + end + + def seed_once(model, constraints, data) + @seeder.seed_once(model, constraints, data) end private @@ -33,18 +44,28 @@ def run_file(filename) puts "\n== Seed from #{filename}" unless SeedDo.quiet ActiveRecord::Base.transaction do - open(filename) do |file| - chunked_ruby = +'' - file.each_line do |line| - if line == "# BREAK EVAL\n" - eval(chunked_ruby) - chunked_ruby = +'' - else - chunked_ruby << line - end + @seeder.with_seed_file { _run_file(filename) } + end + end + + def build_seeder(bulk) + return SeedDo::BulkSeeder.new(batch_size: bulk.fetch(:batch_size, 1000)) if bulk.is_a?(Hash) + + bulk ? SeedDo::BulkSeeder.new : SeedDo::Seeder.new + end + + def _run_file(filename) + open(filename) do |file| + chunked_ruby = +'' + file.each_line do |line| + if line == "# BREAK EVAL\n" + eval(chunked_ruby) + chunked_ruby = +'' + else + chunked_ruby << line end - eval(chunked_ruby) unless chunked_ruby == '' end + eval(chunked_ruby) unless chunked_ruby == '' end end diff --git a/lib/seed-do/seeder.rb b/lib/seed-do/seeder.rb index d03a6ff..9aa7024 100644 --- a/lib/seed-do/seeder.rb +++ b/lib/seed-do/seeder.rb @@ -8,30 +8,31 @@ module SeedDo # # @see ActiveRecordExtension class Seeder - # @param [ActiveRecord::Base] model_class The model to be seeded - # @param [Array] constraints A list of attributes which identify a particular seed. If - # a record with these attributes already exists then it will be updated rather than created. - # @param [Array] data Each item in this array is a hash containing attributes for a - # particular record. - # @param [Hash] options - # @option options [Boolean] :quiet (SeedDo.quiet) If true, output will be silenced - # @option options [Boolean] :insert_only (false) If true then existing records which match the - # constraints will not be updated, even if the seed data has changed - def initialize(model_class, constraints, data, options = {}) + def seed(model_class, constraints, data) + seed_records(model_class, constraints, data) + end + + def seed_once(model_class, constraints, data) + seed_records(model_class, constraints, data, insert_only: true) + end + + def with_seed_file + yield + end + + private + + # Insert/update the records as appropriate. Validation is skipped while saving. + # @return [Array] The records which have been seeded + def seed_records(model_class, constraints, data, options = {}) @model_class = model_class @constraints = constraints.to_a.empty? ? [:id] : constraints @data = data.to_a || [] @options = options.symbolize_keys - @options[:quiet] ||= SeedDo.quiet - validate_constraints! validate_data! - end - # Insert/update the records as appropriate. Validation is skipped while saving. - # @return [Array] The records which have been seeded - def seed records = @model_class.transaction do @data.map { |record_data| seed_record(record_data.symbolize_keys) } end @@ -39,8 +40,6 @@ def seed records end - private - def validate_constraints! unknown_columns = @constraints.map(&:to_s) - @model_class.column_names return if unknown_columns.empty? @@ -62,7 +61,7 @@ def seed_record(data) record = find_or_initialize_record(data) return if @options[:insert_only] && !record.new_record? - puts " - #{@model_class} #{data.inspect}" unless @options[:quiet] + puts " - #{@model_class} #{data.inspect}" unless SeedDo.quiet record.assign_attributes(data) record.save(validate: false) || raise(ActiveRecord::RecordNotSaved, 'Record not saved!') diff --git a/spec/bulk_seeder_spec.rb b/spec/bulk_seeder_spec.rb new file mode 100644 index 0000000..5837a1a --- /dev/null +++ b/spec/bulk_seeder_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe SeedDo::BulkSeeder do + before(:all) do + skip 'Bulk mode is not supported on databases without insert conflict target support' unless ActiveRecord::Base.connection.supports_insert_conflict_target? + end + + around do |example| + original_quiet = SeedDo.quiet + example.run + SeedDo.quiet = original_quiet + end + + it 'clears the buffer after processing' do + bulk_seeder = described_class.new(batch_size: 2) + + bulk_seeder.with_seed_file do + bulk_seeder.seed(BulkSeededModel, [:title], [{ title: 'Bulk 1', login: 'bulk1' }]) + expect(bulk_seeder.buffer).not_to be_nil + end + + expect(bulk_seeder.buffer).to be_nil + end + + it 'separates flushes by operation type' do + bulk_seeder = described_class.new(batch_size: 10) + + expect(BulkSeededModel).to receive(:upsert_all).twice.and_call_original + + bulk_seeder.with_seed_file do + bulk_seeder.seed(BulkSeededModel, [:title], [{ title: 'Bulk 1', login: 'bulk1' }]) + bulk_seeder.seed(BulkSeededModel, [:title], [{ title: 'Bulk 2', login: 'bulk2' }]) + bulk_seeder.seed_once(BulkSeededModel, [:title], [{ title: 'Bulk 3', login: 'bulk3' }]) + end + end + + it 'logs buffered seed operations when quiet is false' do + SeedDo.quiet = false + bulk_seeder = described_class.new(batch_size: 10) + + output = capture_stdout do + bulk_seeder.with_seed_file do + bulk_seeder.seed(BulkSeededModel, [:title], [{ title: 'Bulk 1', login: 'bulk1' }]) + bulk_seeder.seed_once(BulkSeededModel, [:title], [{ title: 'Bulk 2', login: 'bulk2' }]) + end + end + + expect(output).to include(' - BulkSeededModel [') + expect(output).to include('Bulk 1') + expect(output).to include('bulk1') + expect(output).to include('Bulk 2') + expect(output).to include('bulk2') + expect(output.lines.count).to eq(2) + end + + it 'does not log buffered seed operations when quiet is true' do + SeedDo.quiet = true + bulk_seeder = described_class.new(batch_size: 10) + + expect do + bulk_seeder.with_seed_file do + bulk_seeder.seed(BulkSeededModel, [:title], [{ title: 'Bulk 1', login: 'bulk1' }]) + end + end.not_to output.to_stdout + end + + def capture_stdout + original_stdout = $stdout + $stdout = StringIO.new + yield + $stdout.string + ensure + $stdout = original_stdout + end +end diff --git a/spec/bulk_spec.rb b/spec/bulk_spec.rb new file mode 100644 index 0000000..87a1ead --- /dev/null +++ b/spec/bulk_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe 'Bulk Insertion' do + before(:all) do + skip 'Bulk mode is not supported on databases without insert conflict target support' unless ActiveRecord::Base.connection.supports_insert_conflict_target? + end + it 'uses upsert_all when bulk option is true' do + expect(BulkSeededModel).to receive(:upsert_all).with( + contain_exactly( + hash_including('title' => 'Bulk 1', 'login' => 'bulk1'), + hash_including('title' => 'Bulk 2', 'login' => 'bulk2') + ), + hash_including(unique_by: [:title]) + ).and_call_original + + SeedDo.seed("#{File.dirname(__FILE__)}/fixtures", /bulk_insert/, bulk: true) + + expect(BulkSeededModel.count).to eq(2) + item1 = BulkSeededModel.find_by(title: 'Bulk 1') + expect(item1.login).to eq 'bulk1' + item2 = BulkSeededModel.find_by(title: 'Bulk 2') + expect(item2.login).to eq 'bulk2' + end + + it 'uses upsert_all with skip option for seed_once in bulk mode' do + # Pre-create record + BulkSeededModel.create!(title: 'Existing', login: 'original') + + expect(BulkSeededModel).to receive(:upsert_all).with( + contain_exactly( + hash_including('title' => 'Existing', 'login' => 'new'), + hash_including('title' => 'New', 'login' => 'created') + ), + hash_including(unique_by: [:title], on_duplicate: :skip) + ).and_call_original + + SeedDo.seed("#{File.dirname(__FILE__)}/fixtures", /bulk_seed_once/, bulk: true) + + expect(BulkSeededModel.count).to eq(2) + existing = BulkSeededModel.find_by(title: 'Existing') + expect(existing.login).to eq 'original' # Should NOT change + new_rec = BulkSeededModel.find_by(title: 'New') + expect(new_rec.login).to eq 'created' + end + + it 'respects batch_size option when specified' do + # With batch_size: 10, 25 records should result in 3 upsert_all calls (10, 10, 5) + call_count = 0 + allow(BulkSeededModel).to receive(:upsert_all).and_wrap_original do |method, data, **kwargs| + call_count += 1 + method.call(data, **kwargs) + end + + SeedDo.seed("#{File.dirname(__FILE__)}/fixtures", /bulk_large/, bulk: { batch_size: 10 }) + + expect(call_count).to eq(3) + expect(BulkSeededModel.count).to eq(25) + + # Verify all records were created + (1..25).each do |i| + record = BulkSeededModel.find_by(title: "Bulk #{i}") + expect(record).to be_present + expect(record.login).to eq "bulk#{i}" + end + end + + it 'uses default batch_size of 1000 when bulk: true' do + call_count = 0 + allow(BulkSeededModel).to receive(:upsert_all).and_wrap_original do |method, data, **kwargs| + call_count += 1 + method.call(data, **kwargs) + end + + SeedDo.seed("#{File.dirname(__FILE__)}/fixtures", /bulk_insert/, bulk: true) + + # With default batch_size of 1000, 2 records should result in 1 upsert_all call + expect(call_count).to eq(1) + expect(BulkSeededModel.count).to eq(2) + end + + it 'splits large dataset across multiple batches with custom batch_size' do + call_count = 0 + batch_sizes = [] + allow(BulkSeededModel).to receive(:upsert_all).and_wrap_original do |method, data, **options| + call_count += 1 + batch_sizes << data.size + method.call(data, **options) + end + + SeedDo.seed("#{File.dirname(__FILE__)}/fixtures", /bulk_large/, bulk: { batch_size: 7 }) + + # With batch_size: 7, 25 records should result in 4 calls: [7, 7, 7, 4] + expect(call_count).to eq(4) + expect(batch_sizes).to eq([7, 7, 7, 4]) + expect(BulkSeededModel.count).to eq(25) + end +end diff --git a/spec/fixtures/bulk_insert.rb b/spec/fixtures/bulk_insert.rb new file mode 100644 index 0000000..d13b9c9 --- /dev/null +++ b/spec/fixtures/bulk_insert.rb @@ -0,0 +1,9 @@ +BulkSeededModel.seed(:title) do |s| + s.title = 'Bulk 1' + s.login = 'bulk1' +end + +BulkSeededModel.seed(:title) do |s| + s.title = 'Bulk 2' + s.login = 'bulk2' +end diff --git a/spec/fixtures/bulk_large.rb b/spec/fixtures/bulk_large.rb new file mode 100644 index 0000000..ab34b1f --- /dev/null +++ b/spec/fixtures/bulk_large.rb @@ -0,0 +1,7 @@ +# Create 25 records for batch_size testing +(1..25).each do |i| + BulkSeededModel.seed(:title) do |s| + s.title = "Bulk #{i}" + s.login = "bulk#{i}" + end +end diff --git a/spec/fixtures/bulk_seed_once.rb b/spec/fixtures/bulk_seed_once.rb new file mode 100644 index 0000000..ade6561 --- /dev/null +++ b/spec/fixtures/bulk_seed_once.rb @@ -0,0 +1,9 @@ +BulkSeededModel.seed_once(:title) do |s| + s.title = 'Existing' + s.login = 'new' +end + +BulkSeededModel.seed_once(:title) do |s| + s.title = 'New' + s.login = 'created' +end diff --git a/spec/runner_spec.rb b/spec/runner_spec.rb index f6e6c09..047a4f9 100644 --- a/spec/runner_spec.rb +++ b/spec/runner_spec.rb @@ -1,12 +1,38 @@ require 'spec_helper' +require 'tmpdir' describe SeedDo::Runner do + it 'returns Seeder when no runner is active' do + expect(SeedDo.current_seeder).to be_a(SeedDo::Seeder) + end + it 'should seed data from Ruby and gzipped Ruby files in the given fixtures directory' do SeedDo.seed(File.dirname(__FILE__) + '/fixtures') expect(SeededModel.find(1).title).to eq 'Foo' expect(SeededModel.find(2).title).to eq 'Bar' expect(SeededModel.find(3).title).to eq 'Baz' + expect(SeedDo.current_seeder).to be_a(SeedDo::Seeder) + end + + it 'uses a bulk seed runner while bulk seeding is active' do + skip 'Test only runs on databases with insert conflict target support' unless ActiveRecord::Base.connection.supports_insert_conflict_target? + + seen_seeder_classes = [] + + Dir.mktmpdir do |dir| + fixture_path = File.join(dir, 'current_seeder.rb') + File.write(fixture_path, <<~RUBY) + CURRENT_SEEDER_CLASSES << SeedDo.current_seeder.class + RUBY + + stub_const('CURRENT_SEEDER_CLASSES', seen_seeder_classes) + + SeedDo.seed(dir, nil, bulk: true) + end + + expect(seen_seeder_classes).to eq([SeedDo::BulkSeeder]) + expect(SeedDo.current_seeder).to be_a(SeedDo::Seeder) end it 'should seed only the data which matches the filter, if one is given' do @@ -21,4 +47,34 @@ SeedDo.seed expect(SeededModel.count).to eq 3 end + + describe 'bulk mode validation' do + context 'when database does not support insert conflict target' do + it 'raises ArgumentError when bulk option is true' do + skip 'Test only runs on databases without insert conflict target support' if ActiveRecord::Base.connection.supports_insert_conflict_target? + + expect do + SeedDo::Runner.new(File.dirname(__FILE__) + '/fixtures', nil, bulk: true) + end.to raise_error(ArgumentError, /Bulk mode is not supported/) + end + + it 'raises ArgumentError when bulk option is a hash' do + skip 'Test only runs on databases without insert conflict target support' if ActiveRecord::Base.connection.supports_insert_conflict_target? + + expect do + SeedDo::Runner.new(File.dirname(__FILE__) + '/fixtures', nil, bulk: { batch_size: 500 }) + end.to raise_error(ArgumentError, /Bulk mode is not supported/) + end + end + + context 'when database supports insert conflict target' do + it 'does not raise error when bulk option is true' do + skip 'Test only runs on databases with insert conflict target support' unless ActiveRecord::Base.connection.supports_insert_conflict_target? + + expect do + SeedDo::Runner.new(File.dirname(__FILE__) + '/fixtures', nil, bulk: true) + end.not_to raise_error + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 25bf42c..d08efc1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -5,6 +5,8 @@ SeedDo.quiet = true +Dir[File.expand_path('support/models/*.rb', __dir__)].each { |file| require file } + ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + '/../debug.log') ENV['RAILS_ENV'] ||= 'test' ENV['DB'] ||= 'sqlite3' @@ -20,6 +22,14 @@ t.column :title, :string end + create_table :bulk_seeded_models, force: true do |t| + t.column :login, :string + t.column :first_name, :string + t.column :last_name, :string + t.column :title, :string + t.index :title, unique: true + end + create_table :seeded_model_no_primary_keys, id: false, force: true do |t| t.column :id, :string end @@ -31,21 +41,9 @@ execute('ALTER TABLE seeded_model_no_sequences ADD PRIMARY KEY (id)') if ENV['DB'] == 'postgresql' end -class SeededModel < ActiveRecord::Base - validates_presence_of :title - attr_accessor :fail_to_save - - before_save { throw(:abort) if fail_to_save } -end - -class SeededModelNoPrimaryKey < ActiveRecord::Base # rubocop:disable Style/OneClassPerFile -end - -class SeededModelNoSequence < ActiveRecord::Base # rubocop:disable Style/OneClassPerFile -end - RSpec.configure do |config| config.before do SeededModel.delete_all + BulkSeededModel.delete_all end end diff --git a/spec/support/models/bulk_seeded_model.rb b/spec/support/models/bulk_seeded_model.rb new file mode 100644 index 0000000..9f40c31 --- /dev/null +++ b/spec/support/models/bulk_seeded_model.rb @@ -0,0 +1,2 @@ +class BulkSeededModel < ActiveRecord::Base +end diff --git a/spec/support/models/seeded_model.rb b/spec/support/models/seeded_model.rb new file mode 100644 index 0000000..a8f7cb3 --- /dev/null +++ b/spec/support/models/seeded_model.rb @@ -0,0 +1,6 @@ +class SeededModel < ActiveRecord::Base + validates_presence_of :title + attr_accessor :fail_to_save + + before_save { throw(:abort) if fail_to_save } +end diff --git a/spec/support/models/seeded_model_no_primary_key.rb b/spec/support/models/seeded_model_no_primary_key.rb new file mode 100644 index 0000000..466bca5 --- /dev/null +++ b/spec/support/models/seeded_model_no_primary_key.rb @@ -0,0 +1,2 @@ +class SeededModelNoPrimaryKey < ActiveRecord::Base +end diff --git a/spec/support/models/seeded_model_no_sequence.rb b/spec/support/models/seeded_model_no_sequence.rb new file mode 100644 index 0000000..4af224f --- /dev/null +++ b/spec/support/models/seeded_model_no_sequence.rb @@ -0,0 +1,2 @@ +class SeededModelNoSequence < ActiveRecord::Base +end