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.
- 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
|
|