active_record_data_loader 0.1.1 → 1.1.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -7
  3. data/.travis.yml +16 -3
  4. data/Appraisals +2 -2
  5. data/CHANGELOG.md +24 -0
  6. data/Gemfile.lock +56 -46
  7. data/README.md +182 -4
  8. data/Rakefile +2 -0
  9. data/active_record_data_loader.gemspec +5 -3
  10. data/config/database.yml +9 -0
  11. data/config/database.yml.travis +5 -0
  12. data/docker-compose.yml +18 -0
  13. data/gemfiles/activerecord_6.gemfile +1 -1
  14. data/gemfiles/rails.gemfile +7 -0
  15. data/lib/active_record_data_loader.rb +23 -14
  16. data/lib/active_record_data_loader/active_record/belongs_to_configuration.rb +13 -4
  17. data/lib/active_record_data_loader/active_record/column_configuration.rb +14 -4
  18. data/lib/active_record_data_loader/active_record/datetime_value_generator.rb +21 -0
  19. data/lib/active_record_data_loader/active_record/enum_value_generator.rb +25 -3
  20. data/lib/active_record_data_loader/active_record/integer_value_generator.rb +1 -1
  21. data/lib/active_record_data_loader/active_record/model_data_generator.rb +20 -4
  22. data/lib/active_record_data_loader/active_record/per_row_value_cache.rb +33 -0
  23. data/lib/active_record_data_loader/active_record/polymorphic_belongs_to_configuration.rb +9 -1
  24. data/lib/active_record_data_loader/active_record/text_value_generator.rb +1 -1
  25. data/lib/active_record_data_loader/bulk_insert_strategy.rb +1 -6
  26. data/lib/active_record_data_loader/configuration.rb +17 -3
  27. data/lib/active_record_data_loader/copy_strategy.rb +11 -14
  28. data/lib/active_record_data_loader/dsl/belongs_to_association.rb +15 -0
  29. data/lib/active_record_data_loader/dsl/model.rb +6 -1
  30. data/lib/active_record_data_loader/dsl/polymorphic_association.rb +4 -2
  31. data/lib/active_record_data_loader/loader.rb +44 -12
  32. data/lib/active_record_data_loader/version.rb +1 -1
  33. metadata +46 -13
  34. data/script/ci_build.sh +0 -6
data/Rakefile CHANGED
@@ -3,8 +3,10 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
5
  require "rubocop/rake_task"
6
+ require "coveralls/rake/task"
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
9
  RuboCop::RakeTask.new(:rubocop)
10
+ Coveralls::RakeTask.new
9
11
 
10
12
  task default: [:spec, :rubocop]
@@ -30,18 +30,20 @@ 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
39
  spec.add_development_dependency "coveralls"
40
+ spec.add_development_dependency "mysql2"
40
41
  spec.add_development_dependency "pg"
41
42
  spec.add_development_dependency "pry"
42
- spec.add_development_dependency "rake", "~> 12.0"
43
+ spec.add_development_dependency "rake", "~> 13.0"
43
44
  spec.add_development_dependency "rspec", "~> 3.0"
44
45
  spec.add_development_dependency "rspec-collection_matchers"
45
46
  spec.add_development_dependency "rubocop"
46
47
  spec.add_development_dependency "sqlite3"
48
+ spec.add_development_dependency "timecop"
47
49
  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"
@@ -5,3 +5,8 @@ postgres:
5
5
  sqlite3:
6
6
  adapter: "sqlite3"
7
7
  database: ":memory:"
8
+
9
+ mysql:
10
+ adapter: "mysql2"
11
+ database: "test"
12
+ username: "travis"
@@ -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: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails"
6
+
7
+ gemspec path: "../"
@@ -4,13 +4,16 @@ require "active_record_data_loader/version"
4
4
  require "active_record"
5
5
  require "active_record_data_loader/configuration"
6
6
  require "active_record_data_loader/data_faker"
7
+ require "active_record_data_loader/active_record/per_row_value_cache"
7
8
  require "active_record_data_loader/active_record/integer_value_generator"
8
9
  require "active_record_data_loader/active_record/text_value_generator"
9
10
  require "active_record_data_loader/active_record/enum_value_generator"
11
+ require "active_record_data_loader/active_record/datetime_value_generator"
10
12
  require "active_record_data_loader/active_record/column_configuration"
11
13
  require "active_record_data_loader/active_record/belongs_to_configuration"
12
14
  require "active_record_data_loader/active_record/polymorphic_belongs_to_configuration"
13
15
  require "active_record_data_loader/active_record/model_data_generator"
16
+ require "active_record_data_loader/dsl/belongs_to_association"
14
17
  require "active_record_data_loader/dsl/polymorphic_association"
15
18
  require "active_record_data_loader/dsl/model"
16
19
  require "active_record_data_loader/dsl/definition"
@@ -21,7 +24,7 @@ require "active_record_data_loader/loader"
21
24
  module ActiveRecordDataLoader
22
25
  def self.define(config = ActiveRecordDataLoader.configuration, &block)
23
26
  LoaderProxy.new(
24
- configuration,
27
+ config,
25
28
  ActiveRecordDataLoader::Dsl::Definition.new(config).tap { |l| l.instance_eval(&block) }
26
29
  )
27
30
  end
@@ -41,24 +44,30 @@ module ActiveRecordDataLoader
41
44
  end
42
45
 
43
46
  def load_data
44
- definition.models.map do |m|
45
- generator = ActiveRecordDataLoader::ActiveRecord::ModelDataGenerator.new(
46
- model: m.klass,
47
- column_settings: m.columns,
48
- polymorphic_settings: m.polymorphic_associations
49
- )
47
+ ActiveRecordDataLoader::ActiveRecord::PerRowValueCache.clear
50
48
 
51
- ActiveRecordDataLoader::Loader.load_data(
52
- data_generator: generator,
53
- batch_size: m.batch_size,
54
- total_rows: m.row_count,
55
- logger: configuration.logger
56
- )
57
- end
49
+ definition.models.map { |m| load_model(m) }
58
50
  end
59
51
 
60
52
  private
61
53
 
62
54
  attr_reader :definition, :configuration
55
+
56
+ def load_model(model)
57
+ generator = ActiveRecordDataLoader::ActiveRecord::ModelDataGenerator.new(
58
+ model: model.klass,
59
+ column_settings: model.columns,
60
+ polymorphic_settings: model.polymorphic_associations,
61
+ belongs_to_settings: model.belongs_to_associations,
62
+ connection_factory: configuration.connection_factory
63
+ )
64
+
65
+ ActiveRecordDataLoader::Loader.load_data(
66
+ data_generator: generator,
67
+ batch_size: model.batch_size,
68
+ total_rows: model.row_count,
69
+ configuration: configuration
70
+ )
71
+ end
63
72
  end
64
73
  end
@@ -3,14 +3,15 @@
3
3
  module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
5
  class BelongsToConfiguration
6
- def self.config_for(ar_association:)
6
+ def self.config_for(ar_association:, query: nil)
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).foreign_key_func }
9
+ { ar_association.join_foreign_key.to_sym => new(ar_association, query).foreign_key_func }
10
10
  end
11
11
 
12
- def initialize(ar_association)
12
+ def initialize(ar_association, query)
13
13
  @ar_association = ar_association
14
+ @query = query
14
15
  end
15
16
 
16
17
  def foreign_key_func
@@ -20,7 +21,15 @@ module ActiveRecordDataLoader
20
21
  private
21
22
 
22
23
  def possible_values
23
- @possible_values ||= @ar_association.klass.all.pluck(@ar_association.join_primary_key).to_a
24
+ @possible_values ||= base_query.pluck(@ar_association.join_primary_key).to_a
25
+ end
26
+
27
+ def base_query
28
+ if @query.respond_to?(:call)
29
+ @query.call.all
30
+ else
31
+ @ar_association.klass.all
32
+ end
24
33
  end
25
34
  end
26
35
  end
@@ -9,15 +9,17 @@ module ActiveRecordDataLoader
9
9
  integer: IntegerValueGenerator,
10
10
  string: TextValueGenerator,
11
11
  text: TextValueGenerator,
12
+ datetime: DatetimeValueGenerator,
12
13
  }.freeze
13
14
 
14
- def config_for(model_class:, ar_column:)
15
+ def config_for(model_class:, ar_column:, connection_factory:)
15
16
  raise_error_if_not_supported(model_class, ar_column)
16
17
 
17
18
  {
18
- 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(
19
20
  model_class: model_class,
20
- ar_column: ar_column
21
+ ar_column: ar_column,
22
+ connection_factory: connection_factory
21
23
  ),
22
24
  }
23
25
  end
@@ -25,7 +27,7 @@ module ActiveRecordDataLoader
25
27
  def supported?(model_class:, ar_column:)
26
28
  return false if model_class.reflect_on_association(ar_column.name)
27
29
 
28
- VALUE_GENERATORS.keys.include?(ar_column.type)
30
+ VALUE_GENERATORS.keys.include?(column_type(ar_column))
29
31
  end
30
32
 
31
33
  private
@@ -37,6 +39,14 @@ module ActiveRecordDataLoader
37
39
  Column '#{ar_column.name}' of type '#{ar_column.type}' in model '#{model_class.name}' not supported"
38
40
  ERROR
39
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
40
50
  end
41
51
  end
42
52
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ module ActiveRecord
5
+ class DatetimeValueGenerator
6
+ class << self
7
+ def generator_for(model_class:, ar_column:, connection_factory: nil)
8
+ ->(row) { timestamp(model_class, row) }
9
+ end
10
+
11
+ private
12
+
13
+ def timestamp(model, row_number)
14
+ PerRowValueCache[:datetime].get_or_set(model: model, row: row_number) do
15
+ Time.now.utc
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -4,14 +4,26 @@ 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(model_class, 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)
14
+ def enum_values_for(model_class, enum_type, connection_factory)
15
+ connection = connection_factory.call
16
+
17
+ if connection.adapter_name.downcase.to_sym == :postgresql
18
+ postgres_enum_values_for(model_class, enum_type)
19
+ elsif connection.adapter_name.downcase.to_s.start_with?("mysql")
20
+ mysql_enum_values_for(model_class, enum_type)
21
+ else
22
+ []
23
+ end
24
+ end
25
+
26
+ def postgres_enum_values_for(model_class, enum_type)
15
27
  model_class
16
28
  .connection
17
29
  .execute("SELECT unnest(enum_range(NULL::#{enum_type}))::text")
@@ -19,6 +31,16 @@ module ActiveRecordDataLoader
19
31
  .flatten
20
32
  .compact
21
33
  end
34
+
35
+ def mysql_enum_values_for(_model_class, enum_type)
36
+ enum_type
37
+ .to_s
38
+ .downcase
39
+ .gsub(/\Aenum\(|\)\Z/, "")
40
+ .split(",")
41
+ .map(&:strip)
42
+ .map { |s| s.gsub(/\A'|'\Z/, "") }
43
+ end
22
44
  end
23
45
  end
24
46
  end
@@ -4,7 +4,7 @@ module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
5
  class IntegerValueGenerator
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
  range_limit = [(256**number_of_bytes(ar_column)) / 2 - 1, 1_000_000_000].min
9
9
 
10
10
  -> { rand(0..range_limit) }
@@ -5,11 +5,19 @@ module ActiveRecordDataLoader
5
5
  class ModelDataGenerator
6
6
  attr_reader :table
7
7
 
8
- def initialize(model:, column_settings:, polymorphic_settings: [])
8
+ def initialize(
9
+ model:,
10
+ column_settings:,
11
+ connection_factory:,
12
+ polymorphic_settings: [],
13
+ belongs_to_settings: []
14
+ )
9
15
  @model_class = model
10
16
  @table = model.table_name
11
- @polymorphic_settings = polymorphic_settings
12
17
  @column_settings = column_settings
18
+ @polymorphic_settings = polymorphic_settings
19
+ @belongs_to_settings = belongs_to_settings.map { |s| [s.name, s.query] }.to_h
20
+ @connection_factory = connection_factory
13
21
  end
14
22
 
15
23
  def column_list
@@ -49,7 +57,13 @@ module ActiveRecordDataLoader
49
57
  .columns_hash
50
58
  .reject { |name| name == @model_class.primary_key }
51
59
  .select { |_, c| ColumnConfiguration.supported?(model_class: @model_class, ar_column: c) }
52
- .map { |_, c| ColumnConfiguration.config_for(model_class: @model_class, ar_column: c) }
60
+ .map do |_, c|
61
+ ColumnConfiguration.config_for(
62
+ model_class: @model_class,
63
+ ar_column: c,
64
+ connection_factory: @connection_factory
65
+ )
66
+ end
53
67
  .reduce({}, :merge)
54
68
  end
55
69
 
@@ -58,7 +72,9 @@ module ActiveRecordDataLoader
58
72
  .reflect_on_all_associations
59
73
  .select(&:belongs_to?)
60
74
  .reject(&:polymorphic?)
61
- .map { |assoc| BelongsToConfiguration.config_for(ar_association: assoc) }
75
+ .map do |assoc|
76
+ BelongsToConfiguration.config_for(ar_association: assoc, query: @belongs_to_settings[assoc.name])
77
+ end
62
78
  .reduce({}, :merge)
63
79
  end
64
80
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ module ActiveRecord
5
+ class PerRowValueCache
6
+ class << self
7
+ def [](key)
8
+ caches[key] ||= new
9
+ end
10
+
11
+ def clear
12
+ @caches = {}
13
+ end
14
+
15
+ private
16
+
17
+ def caches
18
+ @caches ||= clear
19
+ end
20
+ end
21
+
22
+ def initialize
23
+ @row_caches = Hash.new { |hash, key| hash[key] = {} }
24
+ end
25
+
26
+ def get_or_set(model:, row:)
27
+ @row_caches[model.name].shift if @row_caches[model.name].size > 1
28
+
29
+ @row_caches[model.name][row] ||= yield
30
+ end
31
+ end
32
+ end
33
+ end
@@ -38,12 +38,20 @@ module ActiveRecordDataLoader
38
38
  def possible_values
39
39
  @possible_values ||= begin
40
40
  values = @settings.models.keys.map do |klass|
41
- [klass.name, klass.all.pluck(klass.primary_key).to_a]
41
+ [klass.name, base_query(klass).pluck(klass.primary_key).to_a]
42
42
  end.to_h
43
43
 
44
44
  @settings.weighted_models.map { |klass| [klass.name, values[klass.name]] }
45
45
  end
46
46
  end
47
+
48
+ def base_query(klass)
49
+ if @settings.queries[klass].respond_to?(:call)
50
+ @settings.queries[klass].call.all
51
+ else
52
+ klass.all
53
+ end
54
+ end
47
55
  end
48
56
  end
49
57
  end
@@ -12,7 +12,7 @@ module ActiveRecordDataLoader
12
12
  }.freeze
13
13
 
14
14
  class << self
15
- def generator_for(model_class:, ar_column:)
15
+ def generator_for(model_class:, ar_column:, connection_factory: nil)
16
16
  scenario = GENERATORS.keys.find { |m| send(m, model_class, ar_column) }
17
17
  generator = GENERATORS.fetch(scenario, -> { SecureRandom.uuid })
18
18