active_record_data_loader 1.0.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +51 -0
  3. data/.github/workflows/codeql-analysis.yml +70 -0
  4. data/.github/workflows/gem-push.yml +29 -0
  5. data/.rubocop.yml +46 -7
  6. data/CHANGELOG.md +37 -1
  7. data/CODE_OF_CONDUCT.md +2 -2
  8. data/Gemfile.lock +72 -72
  9. data/README.md +162 -9
  10. data/Rakefile +8 -2
  11. data/active_record_data_loader.gemspec +8 -6
  12. data/config/database.yml +9 -0
  13. data/docker-compose.yml +18 -0
  14. data/gemfiles/activerecord_6.gemfile +1 -1
  15. data/lib/active_record_data_loader/active_record/{belongs_to_configuration.rb → belongs_to_data_provider.rb} +8 -7
  16. data/lib/active_record_data_loader/active_record/{column_configuration.rb → column_data_provider.rb} +14 -5
  17. data/lib/active_record_data_loader/active_record/datetime_value_generator.rb +1 -1
  18. data/lib/active_record_data_loader/active_record/enum_value_generator.rb +28 -5
  19. data/lib/active_record_data_loader/active_record/integer_value_generator.rb +2 -2
  20. data/lib/active_record_data_loader/active_record/list.rb +35 -0
  21. data/lib/active_record_data_loader/active_record/model_data_generator.rb +74 -6
  22. data/lib/active_record_data_loader/active_record/{polymorphic_belongs_to_configuration.rb → polymorphic_belongs_to_data_provider.rb} +12 -7
  23. data/lib/active_record_data_loader/active_record/text_value_generator.rb +1 -1
  24. data/lib/active_record_data_loader/active_record/unique_index_tracker.rb +67 -0
  25. data/lib/active_record_data_loader/bulk_insert_strategy.rb +16 -8
  26. data/lib/active_record_data_loader/configuration.rb +28 -3
  27. data/lib/active_record_data_loader/connection_handler.rb +52 -0
  28. data/lib/active_record_data_loader/copy_strategy.rb +38 -24
  29. data/lib/active_record_data_loader/data_faker.rb +12 -4
  30. data/lib/active_record_data_loader/dsl/model.rb +19 -2
  31. data/lib/active_record_data_loader/errors.rb +5 -0
  32. data/lib/active_record_data_loader/file_output_adapter.rb +48 -0
  33. data/lib/active_record_data_loader/loader.rb +57 -67
  34. data/lib/active_record_data_loader/null_output_adapter.rb +15 -0
  35. data/lib/active_record_data_loader/table_loader.rb +59 -0
  36. data/lib/active_record_data_loader/version.rb +1 -1
  37. data/lib/active_record_data_loader.rb +12 -36
  38. metadata +52 -15
  39. data/.travis.yml +0 -23
  40. data/config/database.yml.travis +0 -7
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # ActiveRecord Data Loader
1
+ # active_record_data_loader
2
2
 
3
- [![Build Status](https://travis-ci.org/abeiderman/active_record_data_loader.svg?branch=master)](https://travis-ci.org/abeiderman/active_record_data_loader)
3
+ [![Build Status](https://github.com/abeiderman/active_record_data_loader/actions/workflows/build.yml/badge.svg)](https://github.com/abeiderman/active_record_data_loader/actions/workflows/build.yml)
4
4
  [![Coverage Status](https://coveralls.io/repos/github/abeiderman/active_record_data_loader/badge.svg?branch=master&service=github)](https://coveralls.io/github/abeiderman/active_record_data_loader?branch=master)
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/338904b3f7e8d19a3cb1/maintainability)](https://codeclimate.com/github/abeiderman/active_record_data_loader/maintainability)
6
6
 
@@ -10,6 +10,10 @@ Efficiently bulk load data for your ActiveRecord models with a simple DSL.
10
10
 
11
11
  Load, performance, and stress tests often require setting up a realistic amount of data in your database. This gem is intended to help organize that data load and make it more maintainable than having a collection of SQL scripts.
12
12
 
13
+ #### How is this different from using _factory_bot_?
14
+
15
+ This gem is not a replacement for [factory_bot](https://github.com/thoughtbot/factory_bot). It solves a different use case. While _factory_bot_ is great for organizing test data and reducing duplication in your functional tests, _active_record_data_loader_ is focused around bulk loading data for performance tests. The purpose of _active_record_data_loader_ is loading large amounts of data as efficiently as possible while providing a DSL that helps with maintainability.
16
+
13
17
  ## Installation
14
18
 
15
19
  Add this line to your application's Gemfile:
@@ -37,6 +41,7 @@ Polymorphic associations need to be defined explicitly as shown in [Polymorphic
37
41
  ### Basic usage
38
42
 
39
43
  Let's say you have the following models:
44
+
40
45
  ```ruby
41
46
  class Customer < ApplicationRecord
42
47
  end
@@ -47,6 +52,7 @@ end
47
52
  ```
48
53
 
49
54
  The following code will create 10,000 customers and 100,000 orders, and will associate the orders to those customers evenly:
55
+
50
56
  ```ruby
51
57
  data_loader = ActiveRecordDataLoader.define do
52
58
  model Customer do |m|
@@ -63,6 +69,7 @@ data_loader.load_data
63
69
 
64
70
  #### Overriding column values
65
71
  To provide your own values for columns your can provide a lambda or a constant value:
72
+
66
73
  ```ruby
67
74
  data_loader = ActiveRecordDataLoader.define do
68
75
  model Customer do |m|
@@ -87,7 +94,7 @@ In this example, we are creating 25K orders for customers in CAN with a CAD curr
87
94
  data_loader = ActiveRecordDataLoader.define do
88
95
  model Customer do |m|
89
96
  m.count 10_000
90
- m.column :country, -> { %w[CAN MXN USA].sample }
97
+ m.column :country, -> { %w[CAN MEX USA].sample }
91
98
  end
92
99
 
93
100
  model Order do |m|
@@ -95,13 +102,13 @@ data_loader = ActiveRecordDataLoader.define do
95
102
  m.column :currency, "CAD"
96
103
  m.belongs_to :customer, eligible_set: -> { Customer.where(country: "CAN") }
97
104
  end
98
-
105
+
99
106
  model Order do |m|
100
107
  m.count 25_000
101
108
  m.column :currency, "MXN"
102
109
  m.belongs_to :customer, eligible_set: -> { Customer.where(country: "MEX") }
103
110
  end
104
-
111
+
105
112
  model Order do |m|
106
113
  m.count 50_000
107
114
  m.column :currency, "USD"
@@ -117,6 +124,7 @@ data_loader.load_data
117
124
  If you have a polymorphic `belongs_to` association, you will need to define that explicitly for it to be populated.
118
125
 
119
126
  Let's assume the following models where an order could belong to either a person or a business:
127
+
120
128
  ```ruby
121
129
  class Person < ApplicationRecord
122
130
  has_many :orders
@@ -132,6 +140,7 @@ end
132
140
  ```
133
141
 
134
142
  In order to populate the `customer` association in orders, you would specify them like this:
143
+
135
144
  ```ruby
136
145
  data_loader = ActiveRecordDataLoader.define do
137
146
  model Person do |m|
@@ -144,7 +153,7 @@ data_loader = ActiveRecordDataLoader.define do
144
153
 
145
154
  model Order do |m|
146
155
  m.count 100_000
147
-
156
+
148
157
  m.polymorphic :customer do |c|
149
158
  c.model Person
150
159
  c.model Business
@@ -156,6 +165,7 @@ data_loader.load_data
156
165
  ```
157
166
 
158
167
  You can also provide a `weight` to each of the target models if you want to control how they are distributed. If you wanted to have twice as many orders for `Person` than for `Business`, it would look like this:
168
+
159
169
  ```ruby
160
170
  data_loader = ActiveRecordDataLoader.define do
161
171
  model Person do |m|
@@ -168,7 +178,7 @@ data_loader = ActiveRecordDataLoader.define do
168
178
 
169
179
  model Order do |m|
170
180
  m.count 100_000
171
-
181
+
172
182
  m.polymorphic :customer do |c|
173
183
  c.model Person, weight: 2
174
184
  c.model Business, weight: 1
@@ -180,6 +190,7 @@ data_loader.load_data
180
190
  ```
181
191
 
182
192
  Additionaly, you can also provide an `eligible_set` to control which records to limit the association to:
193
+
183
194
  ```ruby
184
195
  data_loader = ActiveRecordDataLoader.define do
185
196
  model Person do |m|
@@ -193,7 +204,7 @@ data_loader = ActiveRecordDataLoader.define do
193
204
 
194
205
  model Order do |m|
195
206
  m.count 100_000
196
-
207
+
197
208
  m.polymorphic :customer do |c|
198
209
  c.model Person, weight: 2
199
210
  c.model Business, weight: 1, eligible_set: -> { Business.where(country: "USA") }
@@ -204,6 +215,148 @@ end
204
215
  data_loader.load_data
205
216
  ```
206
217
 
218
+ ### Unique indexes
219
+
220
+ Unique indexes will be detected automatically and the data generator will attempt to generate unique values for each row. The generator keeps track of unique values previously generated and retries rows with repeating values. Because some columns could be generating random values, retrying can eventually be successful.
221
+
222
+ There are a couple of behaviors you can control regarding preventing duplicates. The first is the number of times to retry a given row with duplicate values (that would fail the unique index/constraint). The second is what to do if a unique value cannot be generated after the retries are exhausted.
223
+
224
+ By default, there will be 5 retries per row and the row will be skipped after all retries are unsuccessful. This means fewer rows than requested may end up being populated on that table.
225
+
226
+ Alternatively, you can choose to raise an error if a unique row cannot be generated. You can also set the number of retries to 0 to not retry at all. If the table in question is a primary target for your testing and will be loaded with a lot of data, you will likely not want to have retries since it could potentially slow down data generation significantly.
227
+
228
+ Here is how to adjust these settings. Here let's assyme that `daily_notes` has a unique index on both `date` and `person_id`:
229
+
230
+ ```ruby
231
+ class Person < ApplicationRecord
232
+ end
233
+
234
+ class DailyNotes < ApplicationRecord
235
+ belongs_to :person
236
+ end
237
+
238
+ data_loader = ActiveRecordDataLoader.define do
239
+ model Person do |m|
240
+ m.count 500
241
+ end
242
+
243
+ model DailyNotes do |m|
244
+ m.count 10_000
245
+ m.max_duplicate_retries 10
246
+ m.do_not_raise_on_duplicates
247
+
248
+ m.column :date, -> { Date.today - rand(20) }
249
+ end
250
+ end
251
+
252
+ data_loader.load_data
253
+ ```
254
+
255
+ In the case above, retrying could be a reasonable choice since the date is generated at random and it's a small number of rows being generated.
256
+
257
+ If you want to disable retrying duplicates altogether and raise an error to fail fast you can specify it like this:
258
+
259
+ ```ruby
260
+ class Person < ApplicationRecord
261
+ end
262
+
263
+ class Skill < ApplicationRecord
264
+ end
265
+
266
+ class SkillRating < ApplicationRecord
267
+ belongs_to :person
268
+ belongs_to :skill
269
+ end
270
+
271
+ data_loader = ActiveRecordDataLoader.define do
272
+ model Person do |m|
273
+ m.count 100_000
274
+ end
275
+
276
+ model Skill do |m|
277
+ m.count 100
278
+ end
279
+
280
+ model SkillRating do |m|
281
+ m.count 10_000_000
282
+ m.max_duplicate_retries 0
283
+ m.raise_on_duplicates
284
+
285
+ m.column :rating, -> { rand(1..10) }
286
+ end
287
+ end
288
+
289
+ data_loader.load_data
290
+ ```
291
+
292
+
293
+ ### Configuration options
294
+
295
+ You can define global configuration options like this:
296
+
297
+ ```ruby
298
+ ActiveRecordDataLoader.configure do |c|
299
+ c.logger = ActiveSupport::Logger.new("my_file.log", level: :debug)
300
+ c.statement_timeout = "5min"
301
+ end
302
+ ```
303
+
304
+ Or you can create a configuration object for the specific data loader instance rather than globally:
305
+
306
+ ```ruby
307
+ config = ActiveRecordDataLoader::Configuration.new(
308
+ c.logger = ActiveSupport::Logger.new("my_file.log", level: :debug)
309
+ c.statement_timeout = "5min"
310
+ )
311
+ loader = ActiveRecordDataLoader.define(config) do
312
+ model Company do |m|
313
+ m.count 10
314
+ end
315
+
316
+ # ... more definitions
317
+ end
318
+ ```
319
+
320
+ #### statement_timeout
321
+
322
+ This is currently only used for Postgres connections to adjust the `statement_timeout` value for the connection. The default is `2min`. Depending on the size of the batches you are loading and overall size of the tables you may need to increase this value:
323
+
324
+ ```ruby
325
+ ActiveRecordDataLoader.configure do |c|
326
+ c.statement_timeout = "5min"
327
+ end
328
+ ```
329
+
330
+ #### connection_factory
331
+
332
+ The `connection_factory` option accepts a lambda that should return a connection object whenever executed. If not specified, the default behavior is to retrieve a connection using `ActiveRecord::Base.connection`. You can configure it like this:
333
+
334
+ ```ruby
335
+ ActiveRecordDataLoader.configure do |c|
336
+ c.connection_factory = -> { MyCustomConnectionHandler.open_connection }
337
+ end
338
+ ```
339
+
340
+ #### output
341
+
342
+ The `output` option accepts an optional file name to write a SQL script with the data loading statements. This script file can then be executed manually to load the data. This can be helpful if you need to load the same data multiple times. For example if you are profiling different alternatives in your code and you want to see how each performs with a fully loaded database. In that case you would want to have the same data starting point for each alternative you evaluate. By generating the script file, it would be significantly faster to load that data over and over by executing the existing script.
343
+
344
+ If `output` is nil or empty, no script file will be written.
345
+
346
+ Example usage:
347
+
348
+ ```ruby
349
+ ActiveRecordDataLoader.configure do |c|
350
+ c.output = "./my_script.sql" # Outputs to the provided file
351
+ end
352
+ ```
353
+
354
+ When using an output script file with Postgres, the resulting script will have `\COPY` commands which reference CSV files that contain the data batches to be copied. The CSV files will be created along side the SQL script and will have a naming convention of using the table name and the rows range for the given batch. For example `./my_script_customers_1_to_1000.csv`. Each `\COPY` command in the SQL file will reference the corresponding CSV file so all you need to do is execute the SQL file using `psql`:
355
+
356
+ ```bash
357
+ psql -h my-db-host -U my_user -f my_script.sql
358
+ ```
359
+
207
360
  ## Development
208
361
 
209
362
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -220,4 +373,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
220
373
 
221
374
  ## Code of Conduct
222
375
 
223
- Everyone interacting in the ActiveRecord Data Loader project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/abeiderman/active_record_data_loader/blob/master/CODE_OF_CONDUCT.md).
376
+ Everyone interacting in the _active_record_data_loader_ project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/abeiderman/active_record_data_loader/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -3,10 +3,16 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
  require "rubocop/rake_task"
6
- require "coveralls/rake/task"
7
6
 
8
7
  RSpec::Core::RakeTask.new(:spec)
9
8
  RuboCop::RakeTask.new(:rubocop)
10
- Coveralls::RakeTask.new
11
9
 
12
10
  task default: [:spec, :rubocop]
11
+
12
+ task :wait_for_test_db do
13
+ require "active_record_data_loader"
14
+ require "./spec/active_record_helper"
15
+
16
+ ActiveRecordHelper.wait_for_mysql
17
+ ActiveRecordHelper.wait_for_postgres
18
+ end
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.name = "active_record_data_loader"
9
9
  spec.version = ActiveRecordDataLoader::VERSION
10
10
  spec.authors = ["Alejandro Beiderman"]
11
- spec.email = ["abeiderman@gmail.com"]
11
+ spec.email = ["active_record_data_loader@ossprojects.dev"]
12
12
 
13
13
  spec.summary = "A utility to bulk load test data for performance testing."
14
14
  spec.description = "A utility to bulk load test data for performance testing."
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
20
20
  spec.metadata["source_code_uri"] = "https://github.com/abeiderman/active_record_data_loader"
21
21
  else
22
22
  raise "RubyGems 2.0 or newer is required to protect against " \
23
- "public gem pushes."
23
+ "public gem pushes."
24
24
  end
25
25
 
26
26
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
@@ -30,19 +30,21 @@ Gem::Specification.new do |spec|
30
30
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
31
31
  spec.require_paths = ["lib"]
32
32
 
33
- spec.required_ruby_version = ">= 2.3.0"
33
+ spec.required_ruby_version = ">= 2.5.0"
34
34
 
35
- spec.add_dependency "activerecord", ">= 4.0"
35
+ spec.add_dependency "activerecord", ">= 5.0"
36
36
 
37
37
  spec.add_development_dependency "appraisal"
38
38
  spec.add_development_dependency "bundler", ">= 1.16"
39
- spec.add_development_dependency "coveralls"
39
+ spec.add_development_dependency "mysql2"
40
40
  spec.add_development_dependency "pg"
41
41
  spec.add_development_dependency "pry"
42
- spec.add_development_dependency "rake", "~> 12.0"
42
+ spec.add_development_dependency "rake", "~> 13.0"
43
43
  spec.add_development_dependency "rspec", "~> 3.0"
44
44
  spec.add_development_dependency "rspec-collection_matchers"
45
45
  spec.add_development_dependency "rubocop"
46
+ spec.add_development_dependency "simplecov"
47
+ spec.add_development_dependency "simplecov-lcov"
46
48
  spec.add_development_dependency "sqlite3"
47
49
  spec.add_development_dependency "timecop"
48
50
  end
data/config/database.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  postgres:
2
2
  adapter: "postgresql"
3
3
  host: "127.0.0.1"
4
+ port: "2345"
4
5
  database: "test"
5
6
  username: "test"
6
7
  password: "test"
@@ -8,3 +9,11 @@ postgres:
8
9
  sqlite3:
9
10
  adapter: "sqlite3"
10
11
  database: ":memory:"
12
+
13
+ mysql:
14
+ adapter: "mysql2"
15
+ host: "127.0.0.1"
16
+ port: "3306"
17
+ database: "test"
18
+ username: "test"
19
+ password: "test"
@@ -0,0 +1,18 @@
1
+ version: "3.9"
2
+ services:
3
+ postgres:
4
+ image: postgres:11
5
+ ports:
6
+ - "2345:5432"
7
+ environment:
8
+ - POSTGRES_USER=test
9
+ - POSTGRES_PASSWORD=test
10
+ mysql:
11
+ image: mysql:5
12
+ ports:
13
+ - "3306:3306"
14
+ environment:
15
+ - MYSQL_ROOT_PASSWORD=test
16
+ - MYSQL_USER=test
17
+ - MYSQL_PASSWORD=test
18
+ - MYSQL_DATABASE=test
@@ -2,6 +2,6 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "6.0.0.rc1"
5
+ gem "activerecord", "~>6.1"
6
6
 
7
7
  gemspec path: "../"
@@ -2,30 +2,31 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
- class BelongsToConfiguration
6
- def self.config_for(ar_association:, query: nil)
5
+ class BelongsToDataProvider
6
+ def self.provider_for(ar_association:, query: nil, strategy: :random)
7
7
  raise "#{name} does not support polymorphic associations" if ar_association.polymorphic?
8
8
 
9
- { ar_association.join_foreign_key.to_sym => new(ar_association, query).foreign_key_func }
9
+ { ar_association.join_foreign_key.to_sym => new(ar_association, query, strategy).foreign_key_func }
10
10
  end
11
11
 
12
- def initialize(ar_association, query)
12
+ def initialize(ar_association, query, strategy)
13
13
  @ar_association = ar_association
14
14
  @query = query
15
+ @strategy = strategy
15
16
  end
16
17
 
17
18
  def foreign_key_func
18
- -> { possible_values.sample }
19
+ -> { possible_values.next }
19
20
  end
20
21
 
21
22
  private
22
23
 
23
24
  def possible_values
24
- @possible_values ||= base_query.pluck(@ar_association.join_primary_key).to_a
25
+ @possible_values ||= List.for(base_query.pluck(@ar_association.join_primary_key), strategy: @strategy)
25
26
  end
26
27
 
27
28
  def base_query
28
- if @query&.respond_to?(:call)
29
+ if @query.respond_to?(:call)
29
30
  @query.call.all
30
31
  else
31
32
  @ar_association.klass.all
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
- class ColumnConfiguration
5
+ class ColumnDataProvider
6
6
  class << self
7
7
  VALUE_GENERATORS = {
8
8
  enum: EnumValueGenerator,
@@ -12,13 +12,14 @@ module ActiveRecordDataLoader
12
12
  datetime: DatetimeValueGenerator,
13
13
  }.freeze
14
14
 
15
- def config_for(model_class:, ar_column:)
15
+ def provider_for(model_class:, ar_column:, connection_factory:)
16
16
  raise_error_if_not_supported(model_class, ar_column)
17
17
 
18
18
  {
19
- ar_column.name.to_sym => VALUE_GENERATORS[ar_column.type].generator_for(
19
+ ar_column.name.to_sym => VALUE_GENERATORS[column_type(ar_column)].generator_for(
20
20
  model_class: model_class,
21
- ar_column: ar_column
21
+ ar_column: ar_column,
22
+ connection_factory: connection_factory
22
23
  ),
23
24
  }
24
25
  end
@@ -26,7 +27,7 @@ module ActiveRecordDataLoader
26
27
  def supported?(model_class:, ar_column:)
27
28
  return false if model_class.reflect_on_association(ar_column.name)
28
29
 
29
- VALUE_GENERATORS.keys.include?(ar_column.type)
30
+ VALUE_GENERATORS.keys.include?(column_type(ar_column))
30
31
  end
31
32
 
32
33
  private
@@ -38,6 +39,14 @@ module ActiveRecordDataLoader
38
39
  Column '#{ar_column.name}' of type '#{ar_column.type}' in model '#{model_class.name}' not supported"
39
40
  ERROR
40
41
  end
42
+
43
+ def column_type(ar_column)
44
+ if ar_column.type == :string && ar_column.sql_type.to_s.downcase.start_with?("enum")
45
+ :enum
46
+ else
47
+ ar_column.type
48
+ end
49
+ end
41
50
  end
42
51
  end
43
52
  end
@@ -4,7 +4,7 @@ module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
5
  class DatetimeValueGenerator
6
6
  class << self
7
- def generator_for(model_class:, ar_column:)
7
+ def generator_for(model_class:, ar_column:, connection_factory: nil)
8
8
  ->(row) { timestamp(model_class, row) }
9
9
  end
10
10
 
@@ -4,21 +4,44 @@ module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
5
  class EnumValueGenerator
6
6
  class << self
7
- def generator_for(model_class:, ar_column:)
8
- values = enum_values_for(model_class, ar_column.sql_type)
7
+ def generator_for(model_class:, ar_column:, connection_factory:)
8
+ values = enum_values_for(ar_column.sql_type, connection_factory)
9
9
  -> { values.sample }
10
10
  end
11
11
 
12
12
  private
13
13
 
14
- def enum_values_for(model_class, enum_type)
15
- model_class
16
- .connection
14
+ def enum_values_for(enum_type, connection_factory)
15
+ connection = connection_factory.call
16
+
17
+ if connection.adapter_name.downcase.to_sym == :postgresql
18
+ postgres_enum_values_for(connection, enum_type)
19
+ elsif connection.adapter_name.downcase.to_s.start_with?("mysql")
20
+ mysql_enum_values_for(enum_type)
21
+ else
22
+ []
23
+ end
24
+ ensure
25
+ connection&.close
26
+ end
27
+
28
+ def postgres_enum_values_for(connection, enum_type)
29
+ connection
17
30
  .execute("SELECT unnest(enum_range(NULL::#{enum_type}))::text")
18
31
  .map(&:values)
19
32
  .flatten
20
33
  .compact
21
34
  end
35
+
36
+ def mysql_enum_values_for(enum_type)
37
+ enum_type
38
+ .to_s
39
+ .downcase
40
+ .gsub(/\Aenum\(|\)\Z/, "")
41
+ .split(",")
42
+ .map(&:strip)
43
+ .map { |s| s.gsub(/\A'|'\Z/, "") }
44
+ end
22
45
  end
23
46
  end
24
47
  end
@@ -4,8 +4,8 @@ module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
5
  class IntegerValueGenerator
6
6
  class << self
7
- def generator_for(model_class:, ar_column:)
8
- range_limit = [(256**number_of_bytes(ar_column)) / 2 - 1, 1_000_000_000].min
7
+ def generator_for(model_class:, ar_column:, connection_factory: nil)
8
+ range_limit = [((256**number_of_bytes(ar_column)) / 2) - 1, 1_000_000_000].min
9
9
 
10
10
  -> { rand(0..range_limit) }
11
11
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ module ActiveRecord
5
+ class List
6
+ def self.for(enumerable, strategy: :random)
7
+ if strategy == :cycle
8
+ Cycle.new(enumerable)
9
+ else
10
+ Random.new(enumerable)
11
+ end
12
+ end
13
+
14
+ class Random
15
+ def initialize(enumerable)
16
+ @list = enumerable
17
+ end
18
+
19
+ def next
20
+ @list.sample
21
+ end
22
+ end
23
+
24
+ class Cycle
25
+ def initialize(enumerable)
26
+ @list = enumerable.cycle
27
+ end
28
+
29
+ def next
30
+ @list.next
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end