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.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -7
- data/.travis.yml +16 -3
- data/Appraisals +2 -2
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +56 -46
- data/README.md +182 -4
- data/Rakefile +2 -0
- data/active_record_data_loader.gemspec +5 -3
- data/config/database.yml +9 -0
- data/config/database.yml.travis +5 -0
- data/docker-compose.yml +18 -0
- data/gemfiles/activerecord_6.gemfile +1 -1
- data/gemfiles/rails.gemfile +7 -0
- data/lib/active_record_data_loader.rb +23 -14
- data/lib/active_record_data_loader/active_record/belongs_to_configuration.rb +13 -4
- data/lib/active_record_data_loader/active_record/column_configuration.rb +14 -4
- data/lib/active_record_data_loader/active_record/datetime_value_generator.rb +21 -0
- data/lib/active_record_data_loader/active_record/enum_value_generator.rb +25 -3
- data/lib/active_record_data_loader/active_record/integer_value_generator.rb +1 -1
- data/lib/active_record_data_loader/active_record/model_data_generator.rb +20 -4
- data/lib/active_record_data_loader/active_record/per_row_value_cache.rb +33 -0
- data/lib/active_record_data_loader/active_record/polymorphic_belongs_to_configuration.rb +9 -1
- data/lib/active_record_data_loader/active_record/text_value_generator.rb +1 -1
- data/lib/active_record_data_loader/bulk_insert_strategy.rb +1 -6
- data/lib/active_record_data_loader/configuration.rb +17 -3
- data/lib/active_record_data_loader/copy_strategy.rb +11 -14
- data/lib/active_record_data_loader/dsl/belongs_to_association.rb +15 -0
- data/lib/active_record_data_loader/dsl/model.rb +6 -1
- data/lib/active_record_data_loader/dsl/polymorphic_association.rb +4 -2
- data/lib/active_record_data_loader/loader.rb +44 -12
- data/lib/active_record_data_loader/version.rb +1 -1
- metadata +46 -13
- data/script/ci_build.sh +0 -6
data/Rakefile
CHANGED
@@ -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.
|
33
|
+
spec.required_ruby_version = ">= 2.5.0"
|
34
34
|
|
35
|
-
spec.add_dependency "activerecord", ">=
|
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", "~>
|
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"
|
data/config/database.yml.travis
CHANGED
data/docker-compose.yml
ADDED
@@ -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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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 ||=
|
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
|
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
|
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(
|
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
|
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
|
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.
|
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
|
|