active_record_data_loader 0.1.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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