seed-do 3.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f41a0a75dff7954f90a7ed6cafe60eef73f0c20490ff9e42952b34e2efe896bc
4
- data.tar.gz: dd04a94f944a7a290d211b16cec2537a3fa34bc2816cee6c1b594d22a91a907c
3
+ metadata.gz: c5486ba5dbc636b111be4e69d838b7f317b233ea9cf4b516d611fdf5070f0780
4
+ data.tar.gz: a04c1e585a72163d13184a317aecf059eb4c21f24d9b3e51fd208debf452a574
5
5
  SHA512:
6
- metadata.gz: ee3af13c18e0354bc04a378db5d0fc6ef344e2af3c617cc5405153200dd37d316be5e31d07234e077719e875c0daa6098878907146c27924ccb2021c9ba36725
7
- data.tar.gz: 36367e0c2457b3d18781824ef255095545b7c441c37468c28c2448413877656c5180edca15da8f8d7309cc2bda78d4d9e2e0e7066afdfe607ec0ad71dc89fd89
6
+ metadata.gz: a5d0cbf12b3790a33f0c362967c4a7b20a85ef7cdb3e9a0c15b47111d0a4fab0ae5912728c1fbab4be32697669f4ed672d221badc00ea5abca06999117e3a4fb
7
+ data.tar.gz: 0011f72cf7c27fcfef5d8dad96ca74525582eaf9ddc01e66dd0b1b575e255d28c72c49db6b81f0523704991ab12545d7a4ebfc75c83fe46ef8ede8a71448d423
data/README.md CHANGED
@@ -142,6 +142,30 @@ Seed files can be run automatically using `rake db:seed_do`. There are two optio
142
142
 
143
143
  You can also do a similar thing in your code by calling `SeedDo.seed(fixture_paths, filter)`.
144
144
 
145
+ ## Bulk upsert
146
+
147
+ If you load a large amount of seed data, you can enable bulk upsert mode:
148
+
149
+ ```ruby
150
+ SeedDo.seed(bulk: true)
151
+ ```
152
+
153
+ In bulk mode, SeedDo buffers seed data for each file and writes it with `upsert_all`, which can significantly reduce the number of queries.
154
+
155
+ You can also control the batch size per `upsert_all` call. The default is `1000`.
156
+
157
+ ```ruby
158
+ SeedDo.seed(bulk: { batch_size: 100 })
159
+ ```
160
+
161
+ ### Requirements and caveats
162
+
163
+ - Bulk mode relies on Rails `upsert_all`
164
+ - The seed constraints are passed to `unique_by`, so the related columns must have a unique index
165
+ - Bulk mode is supported only on databases that support conflict targets, such as PostgreSQL and SQLite
166
+ - Bulk mode is not available on MySQL
167
+ - `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
168
+
145
169
  ## Disable output
146
170
 
147
171
  To disable output from Seed Do, set `SeedDo.quiet = true`.
@@ -200,6 +224,22 @@ DB=postgresql bundle exec rspec
200
224
 
201
225
  The connection paramaters for each of these are specified in spec/connections/, which you can edit if necessary (for example to change the username/password).
202
226
 
227
+ ### Dev Container
228
+
229
+ This repository includes a Dev Container setup for running the test suite locally with Ruby, SQLite, MySQL, and PostgreSQL ready to use.
230
+
231
+ 1. Open the repository in a Dev Container.
232
+ 2. Wait for `bundle install` to finish.
233
+ 3. Run the specs you want:
234
+
235
+ ```
236
+ bundle exec rspec
237
+ DB=mysql2 bundle exec rspec
238
+ DB=postgresql bundle exec rspec
239
+ ```
240
+
241
+ The Dev Container config also sets `MYSQL_HOST=mysql` and `POSTGRES_HOST=postgres` automatically, so the database-backed specs work without editing local machine settings.
242
+
203
243
  ## Original Author
204
244
 
205
245
  [Michael Bleigh](http://www.mbleigh.com/) is the original author
@@ -29,7 +29,7 @@ module SeedDo
29
29
  # { :x => 5, :y => 9, :name => "Office" }
30
30
  # )
31
31
  def seed(*args, &block)
32
- SeedDo::Seeder.new(self, *parse_seed_do_args(args, block)).seed
32
+ SeedDo.current_seeder.seed(self, *parse_seed_do_args(args, block))
33
33
  end
34
34
 
35
35
  # Has the same syntax as {#seed}, but if a record already exists with the same values for
@@ -40,8 +40,7 @@ module SeedDo
40
40
  # Person.seed(:id, :id => 1, :name => "Bob") # => Name changed
41
41
  # Person.seed_once(:id, :id => 1, :name => "Harry") # => Name *not* changed
42
42
  def seed_once(*args, &block)
43
- constraints, data = parse_seed_do_args(args, block)
44
- SeedDo::Seeder.new(self, constraints, data, insert_only: true).seed
43
+ SeedDo.current_seeder.seed_once(self, *parse_seed_do_args(args, block))
45
44
  end
46
45
 
47
46
  private
@@ -0,0 +1,66 @@
1
+ module SeedDo
2
+ # Buffers seeds during a file run and flushes them in bulk.
3
+ class BulkSeeder
4
+ attr_reader :buffer
5
+
6
+ def initialize(batch_size: 1000)
7
+ @batch_size = batch_size
8
+ @buffer = nil
9
+
10
+ validate_support!
11
+ end
12
+
13
+ def seed(model, constraints, data)
14
+ puts " - #{model} #{data.inspect}" unless SeedDo.quiet
15
+ buffer[:seed] << { model: model, constraints: constraints, data: data }
16
+ end
17
+
18
+ def seed_once(model, constraints, data)
19
+ puts " - #{model} #{data.inspect}" unless SeedDo.quiet
20
+ buffer[:seed_once] << { model: model, constraints: constraints, data: data }
21
+ end
22
+
23
+ def with_seed_file
24
+ @buffer = { seed: [], seed_once: [] }
25
+ yield
26
+ process_buffer
27
+ ensure
28
+ @buffer = nil
29
+ end
30
+
31
+ private
32
+
33
+ def validate_support!
34
+ return if ActiveRecord::Base.connection.supports_insert_conflict_target?
35
+
36
+ raise ArgumentError,
37
+ "Bulk mode is not supported for #{ActiveRecord::Base.connection.adapter_name}. " \
38
+ 'The database does not support upsert operations with conflict targets. ' \
39
+ 'Please use SeedDo.seed without the bulk option.'
40
+ end
41
+
42
+ def process_buffer
43
+ return unless buffer
44
+
45
+ buffer.each do |type, operations|
46
+ next if operations.empty?
47
+
48
+ operations.chunk { |operation| [operation[:model], operation[:constraints]] }
49
+ .each do |(model, constraints), chunk|
50
+ flush_chunk(model, constraints, type, chunk)
51
+ end
52
+ end
53
+ end
54
+
55
+ def flush_chunk(model, constraints, type, chunk)
56
+ all_data = chunk.flat_map { |operation| operation[:data] }
57
+
58
+ options = { unique_by: constraints }
59
+ options[:on_duplicate] = :skip if type == :seed_once
60
+
61
+ all_data.each_slice(@batch_size) do |batch|
62
+ model.upsert_all(batch, **options)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,9 +3,7 @@ require 'active_support/core_ext/array/wrap'
3
3
 
4
4
  module SeedDo
5
5
  # Runs seed files.
6
- #
7
- # It is not recommended to use this class directly. Instead, use {SeedDo.seed SeedDo.seed}, which creates
8
- # an instead of {Runner} and calls {#run #run}.
6
+ # 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}.
9
7
  #
10
8
  # @see SeedDo.seed SeedDo.seed
11
9
  class Runner
@@ -13,18 +11,31 @@ module SeedDo
13
11
  # `SeedDo.fixture_paths` if {nil}. If the argument is not an array, it will be wrapped by one.
14
12
  # @param [Regexp] filter If given, only seed files with a file name matching this pattern will
15
13
  # be used
16
- def initialize(fixture_paths = nil, filter = nil)
14
+ # @param [Boolean, Hash] bulk If true, use upsert_all to insert/update records in bulk.
15
+ # If a hash, can include :batch_size (default: 1000) to control the number of records
16
+ # per upsert_all call.
17
+ def initialize(fixture_paths = nil, filter = nil, bulk: false)
17
18
  @fixture_paths = Array.wrap(fixture_paths || SeedDo.fixture_paths)
18
19
  @filter = filter
20
+ @seeder = build_seeder(bulk)
19
21
  end
20
22
 
21
23
  # Run the seed files.
22
24
  def run
25
+ SeedDo.current_seeder = @seeder
23
26
  puts "\n== Filtering seed files against regexp: #{@filter.inspect}" if @filter && !SeedDo.quiet
24
27
 
25
- filenames.each do |filename|
26
- run_file(filename)
27
- end
28
+ filenames.each { |filename| run_file(filename) }
29
+ ensure
30
+ SeedDo.current_seeder = nil
31
+ end
32
+
33
+ def seed(model, constraints, data)
34
+ @seeder.seed(model, constraints, data)
35
+ end
36
+
37
+ def seed_once(model, constraints, data)
38
+ @seeder.seed_once(model, constraints, data)
28
39
  end
29
40
 
30
41
  private
@@ -33,18 +44,28 @@ module SeedDo
33
44
  puts "\n== Seed from #{filename}" unless SeedDo.quiet
34
45
 
35
46
  ActiveRecord::Base.transaction do
36
- open(filename) do |file|
37
- chunked_ruby = +''
38
- file.each_line do |line|
39
- if line == "# BREAK EVAL\n"
40
- eval(chunked_ruby)
41
- chunked_ruby = +''
42
- else
43
- chunked_ruby << line
44
- end
47
+ @seeder.with_seed_file { _run_file(filename) }
48
+ end
49
+ end
50
+
51
+ def build_seeder(bulk)
52
+ return SeedDo::BulkSeeder.new(batch_size: bulk.fetch(:batch_size, 1000)) if bulk.is_a?(Hash)
53
+
54
+ bulk ? SeedDo::BulkSeeder.new : SeedDo::Seeder.new
55
+ end
56
+
57
+ def _run_file(filename)
58
+ open(filename) do |file|
59
+ chunked_ruby = +''
60
+ file.each_line do |line|
61
+ if line == "# BREAK EVAL\n"
62
+ eval(chunked_ruby)
63
+ chunked_ruby = +''
64
+ else
65
+ chunked_ruby << line
45
66
  end
46
- eval(chunked_ruby) unless chunked_ruby == ''
47
67
  end
68
+ eval(chunked_ruby) unless chunked_ruby == ''
48
69
  end
49
70
  end
50
71
 
@@ -8,30 +8,31 @@ module SeedDo
8
8
  #
9
9
  # @see ActiveRecordExtension
10
10
  class Seeder
11
- # @param [ActiveRecord::Base] model_class The model to be seeded
12
- # @param [Array<Symbol>] constraints A list of attributes which identify a particular seed. If
13
- # a record with these attributes already exists then it will be updated rather than created.
14
- # @param [Array<Hash>] data Each item in this array is a hash containing attributes for a
15
- # particular record.
16
- # @param [Hash] options
17
- # @option options [Boolean] :quiet (SeedDo.quiet) If true, output will be silenced
18
- # @option options [Boolean] :insert_only (false) If true then existing records which match the
19
- # constraints will not be updated, even if the seed data has changed
20
- def initialize(model_class, constraints, data, options = {})
11
+ def seed(model_class, constraints, data)
12
+ seed_records(model_class, constraints, data)
13
+ end
14
+
15
+ def seed_once(model_class, constraints, data)
16
+ seed_records(model_class, constraints, data, insert_only: true)
17
+ end
18
+
19
+ def with_seed_file
20
+ yield
21
+ end
22
+
23
+ private
24
+
25
+ # Insert/update the records as appropriate. Validation is skipped while saving.
26
+ # @return [Array<ActiveRecord::Base>] The records which have been seeded
27
+ def seed_records(model_class, constraints, data, options = {})
21
28
  @model_class = model_class
22
29
  @constraints = constraints.to_a.empty? ? [:id] : constraints
23
30
  @data = data.to_a || []
24
31
  @options = options.symbolize_keys
25
32
 
26
- @options[:quiet] ||= SeedDo.quiet
27
-
28
33
  validate_constraints!
29
34
  validate_data!
30
- end
31
35
 
32
- # Insert/update the records as appropriate. Validation is skipped while saving.
33
- # @return [Array<ActiveRecord::Base>] The records which have been seeded
34
- def seed
35
36
  records = @model_class.transaction do
36
37
  @data.map { |record_data| seed_record(record_data.symbolize_keys) }
37
38
  end
@@ -39,8 +40,6 @@ module SeedDo
39
40
  records
40
41
  end
41
42
 
42
- private
43
-
44
43
  def validate_constraints!
45
44
  unknown_columns = @constraints.map(&:to_s) - @model_class.column_names
46
45
  return if unknown_columns.empty?
@@ -62,7 +61,7 @@ module SeedDo
62
61
  record = find_or_initialize_record(data)
63
62
  return if @options[:insert_only] && !record.new_record?
64
63
 
65
- puts " - #{@model_class} #{data.inspect}" unless @options[:quiet]
64
+ puts " - #{@model_class} #{data.inspect}" unless SeedDo.quiet
66
65
 
67
66
  record.assign_attributes(data)
68
67
  record.save(validate: false) || raise(ActiveRecord::RecordNotSaved, 'Record not saved!')
@@ -1,3 +1,3 @@
1
1
  module SeedDo
2
- VERSION = '3.2.0'
2
+ VERSION = '4.0.0'
3
3
  end
data/lib/seed-do.rb CHANGED
@@ -7,6 +7,7 @@ module SeedDo
7
7
  autoload :Seeder, 'seed-do/seeder'
8
8
  autoload :ActiveRecordExtension, 'seed-do/active_record_extension'
9
9
  autoload :BlockHash, 'seed-do/block_hash'
10
+ autoload :BulkSeeder, 'seed-do/bulk_seeder'
10
11
  autoload :Runner, 'seed-do/runner'
11
12
  autoload :Writer, 'seed-do/writer'
12
13
 
@@ -17,12 +18,20 @@ module SeedDo
17
18
  # plugin, SeedDo will set it to contain `Rails.root/db/fixtures` and
18
19
  # `Rails.root/db/fixtures/Rails.env`
19
20
  mattr_accessor :fixture_paths, default: ['db/fixtures']
20
-
21
21
  # Load seed data from files
22
22
  # @param [Array] fixture_paths The paths to look for seed files in
23
23
  # @param [Regexp] filter If given, only filenames matching this expression will be loaded
24
- def self.seed(fixture_paths = SeedDo.fixture_paths, filter = nil)
25
- Runner.new(fixture_paths, filter).run
24
+ # @param [Boolean] bulk If true, bulk insert/upsert will be used
25
+ def self.seed(fixture_paths = SeedDo.fixture_paths, filter = nil, bulk: false)
26
+ Runner.new(fixture_paths, filter, bulk: bulk).run
27
+ end
28
+
29
+ def self.current_seeder
30
+ @current_seeder || Seeder.new
31
+ end
32
+
33
+ def self.current_seeder=(seeder)
34
+ @current_seeder = seeder
26
35
  end
27
36
  end
28
37
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seed-do
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinichi Maeshima
@@ -66,6 +66,7 @@ files:
66
66
  - lib/seed-do.rb
67
67
  - lib/seed-do/active_record_extension.rb
68
68
  - lib/seed-do/block_hash.rb
69
+ - lib/seed-do/bulk_seeder.rb
69
70
  - lib/seed-do/capistrano.rb
70
71
  - lib/seed-do/capistrano3.rb
71
72
  - lib/seed-do/railtie.rb
@@ -94,7 +95,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
94
95
  - !ruby/object:Gem::Version
95
96
  version: '0'
96
97
  requirements: []
97
- rubygems_version: 3.6.9
98
+ rubygems_version: 4.0.6
98
99
  specification_version: 4
99
100
  summary: Easily manage seed data in your Active Record application
100
101
  test_files: []