active_record_data_loader 1.0.2 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +5 -5
  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 +38 -2
  7. data/CODE_OF_CONDUCT.md +2 -2
  8. data/Gemfile.lock +71 -73
  9. data/README.md +162 -9
  10. data/Rakefile +8 -2
  11. data/active_record_data_loader.gemspec +7 -6
  12. data/config/database.yml +2 -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} +2 -2
  17. data/lib/active_record_data_loader/active_record/enum_value_generator.rb +9 -8
  18. data/lib/active_record_data_loader/active_record/integer_value_generator.rb +1 -1
  19. data/lib/active_record_data_loader/active_record/list.rb +47 -0
  20. data/lib/active_record_data_loader/active_record/model_data_generator.rb +62 -7
  21. data/lib/active_record_data_loader/active_record/{polymorphic_belongs_to_configuration.rb → polymorphic_belongs_to_data_provider.rb} +12 -7
  22. data/lib/active_record_data_loader/active_record/unique_index_tracker.rb +67 -0
  23. data/lib/active_record_data_loader/bulk_insert_strategy.rb +16 -8
  24. data/lib/active_record_data_loader/configuration.rb +26 -3
  25. data/lib/active_record_data_loader/connection_handler.rb +52 -0
  26. data/lib/active_record_data_loader/copy_strategy.rb +38 -24
  27. data/lib/active_record_data_loader/data_faker.rb +12 -4
  28. data/lib/active_record_data_loader/dsl/model.rb +19 -2
  29. data/lib/active_record_data_loader/errors.rb +5 -0
  30. data/lib/active_record_data_loader/file_output_adapter.rb +48 -0
  31. data/lib/active_record_data_loader/loader.rb +55 -71
  32. data/lib/active_record_data_loader/null_output_adapter.rb +15 -0
  33. data/lib/active_record_data_loader/table_loader.rb +59 -0
  34. data/lib/active_record_data_loader/version.rb +1 -1
  35. data/lib/active_record_data_loader.rb +11 -38
  36. metadata +51 -29
  37. data/.travis.yml +0 -24
  38. data/config/database.yml.travis +0 -12
@@ -2,20 +2,21 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  module ActiveRecord
5
- class PolymorphicBelongsToConfiguration
6
- def self.config_for(polymorphic_settings:)
5
+ class PolymorphicBelongsToDataProvider
6
+ def self.provider_for(polymorphic_settings:, strategy: :random)
7
7
  ar_association = polymorphic_settings.model_class.reflect_on_association(
8
8
  polymorphic_settings.name
9
9
  )
10
10
  raise "#{name} only supports polymorphic associations" unless ar_association.polymorphic?
11
11
 
12
- new(polymorphic_settings, ar_association).polymorphic_config
12
+ new(polymorphic_settings, ar_association, strategy).polymorphic_config
13
13
  end
14
14
 
15
- def initialize(settings, ar_association)
15
+ def initialize(settings, ar_association, strategy)
16
16
  @settings = settings
17
17
  @ar_association = ar_association
18
18
  @model_count = settings.weighted_models.size
19
+ @strategy = strategy
19
20
  end
20
21
 
21
22
  def polymorphic_config
@@ -32,21 +33,25 @@ module ActiveRecordDataLoader
32
33
  end
33
34
 
34
35
  def foreign_key(row_number)
35
- possible_values[row_number % @model_count][1].sample
36
+ possible_values[row_number % @model_count][1].next
36
37
  end
37
38
 
38
39
  def possible_values
39
40
  @possible_values ||= begin
40
41
  values = @settings.models.keys.map do |klass|
41
- [klass.name, base_query(klass).pluck(klass.primary_key).to_a]
42
+ [klass.name, values_query(klass)]
42
43
  end.to_h
43
44
 
44
45
  @settings.weighted_models.map { |klass| [klass.name, values[klass.name]] }
45
46
  end
46
47
  end
47
48
 
49
+ def values_query(klass)
50
+ List.for(base_query(klass).pluck(klass.primary_key), strategy: @strategy)
51
+ end
52
+
48
53
  def base_query(klass)
49
- if @settings.queries[klass]&.respond_to?(:call)
54
+ if @settings.queries[klass].respond_to?(:call)
50
55
  @settings.queries[klass].call.all
51
56
  else
52
57
  klass.all
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ module ActiveRecord
5
+ class UniqueIndexTracker
6
+ Index = Struct.new(:name, :columns, :column_indexes, keyword_init: true)
7
+
8
+ def initialize(model:, connection_factory:)
9
+ @model = model
10
+ @table = model.table_name
11
+ @unique_indexes = []
12
+ @unique_values_used = {}
13
+ find_unique_indexes(connection_factory)
14
+ end
15
+
16
+ def map_indexed_columns(column_list)
17
+ @unique_indexes = @raw_unique_indexes.map do |index|
18
+ @unique_values_used[index.name] = Set.new
19
+ columns = index.columns.map(&:to_sym)
20
+ Index.new(
21
+ name: index.name,
22
+ columns: columns,
23
+ column_indexes: columns.map { |c| column_list.find_index(c) }
24
+ )
25
+ end
26
+ end
27
+
28
+ def repeating_unique_values?(row)
29
+ @unique_indexes.map do |index|
30
+ values = index.column_indexes.map { |i| row[i] }
31
+ @unique_values_used.fetch(index.name).include?(values)
32
+ end.any?
33
+ end
34
+
35
+ def capture_unique_values(row)
36
+ return unless row.present?
37
+
38
+ @unique_indexes.each do |index|
39
+ values = index.column_indexes.map { |i| row[i] }
40
+ @unique_values_used.fetch(index.name) << values
41
+ end
42
+ row
43
+ end
44
+
45
+ def contained_in_index?(ar_column)
46
+ target_column = if @model.reflect_on_association(ar_column.name)&.belongs_to?
47
+ ar_column.join_foreign_key.to_sym
48
+ else
49
+ ar_column.name.to_sym
50
+ end
51
+
52
+ @raw_unique_indexes.flat_map { |i| i.columns.map(&:to_sym) }.include?(target_column)
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :table
58
+
59
+ def find_unique_indexes(connection_factory)
60
+ connection = connection_factory.call
61
+ @raw_unique_indexes = connection.indexes(table).select(&:unique)
62
+ ensure
63
+ connection&.close
64
+ end
65
+ end
66
+ end
67
+ end
@@ -2,15 +2,18 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  class BulkInsertStrategy
5
- def initialize(data_generator)
5
+ def initialize(data_generator, file_adapter)
6
6
  @data_generator = data_generator
7
+ @file_adapter = file_adapter
7
8
  end
8
9
 
9
10
  def load_batch(row_numbers, connection)
10
- connection.insert(<<~SQL)
11
+ command = <<~SQL
11
12
  INSERT INTO #{quoted_table_name(connection)} (#{column_list(connection)})
12
13
  VALUES #{values(row_numbers, connection)}
13
14
  SQL
15
+ insert(connection: connection, command: command)
16
+ file_adapter.insert(command)
14
17
  end
15
18
 
16
19
  def table_name
@@ -23,7 +26,11 @@ module ActiveRecordDataLoader
23
26
 
24
27
  private
25
28
 
26
- attr_reader :data_generator
29
+ attr_reader :data_generator, :file_adapter
30
+
31
+ def insert(connection:, command:)
32
+ connection.insert(command)
33
+ end
27
34
 
28
35
  def quoted_table_name(connection)
29
36
  @quoted_table_name ||= connection.quote_table_name(data_generator.table)
@@ -38,15 +45,16 @@ module ActiveRecordDataLoader
38
45
 
39
46
  def values(row_numbers, connection)
40
47
  row_numbers
41
- .map { |i| "(#{row_values(i, connection)})" }
48
+ .map { |i| row_values(i, connection) }
49
+ .compact
42
50
  .join(",")
43
51
  end
44
52
 
45
53
  def row_values(row_number, connection)
46
- data_generator
47
- .generate_row(row_number)
48
- .map { |v| connection.quote(v) }
49
- .join(",")
54
+ row = data_generator.generate_row(row_number)
55
+ return unless row.present?
56
+
57
+ "(#{row.map { |v| connection.quote(v) }.join(',')})"
50
58
  end
51
59
  end
52
60
  end
@@ -2,29 +2,52 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  class Configuration
5
- attr_accessor :default_batch_size, :default_row_count, :logger, :statement_timeout, :connection_factory
5
+ attr_accessor :connection_factory, :default_batch_size, :default_row_count,
6
+ :logger, :max_duplicate_retries, :raise_on_duplicates, :statement_timeout
7
+ attr_reader :output
6
8
 
7
9
  def initialize(
8
10
  default_batch_size: 100_000,
9
11
  default_row_count: 1,
10
12
  logger: nil,
11
13
  statement_timeout: "2min",
12
- connection_factory: -> { ::ActiveRecord::Base.connection }
14
+ connection_factory: -> { ::ActiveRecord::Base.connection },
15
+ raise_on_duplicates: false,
16
+ max_duplicate_retries: 5,
17
+ output: nil
13
18
  )
14
19
  @default_batch_size = default_batch_size
15
20
  @default_row_count = default_row_count
16
21
  @logger = logger || default_logger
17
22
  @statement_timeout = statement_timeout
18
23
  @connection_factory = connection_factory
24
+ @raise_on_duplicates = raise_on_duplicates
25
+ @max_duplicate_retries = max_duplicate_retries
26
+ self.output = output
27
+ end
28
+
29
+ def output=(output)
30
+ @output = validate_output(output)
19
31
  end
20
32
 
21
33
  private
22
34
 
35
+ def validate_output(output)
36
+ if output.to_s.blank?
37
+ nil
38
+ elsif output.is_a?(String)
39
+ output
40
+ else
41
+ raise "The output configuration parameter must be a filename meant to be the "\
42
+ "target for the SQL script"
43
+ end
44
+ end
45
+
23
46
  def default_logger
24
47
  if defined?(Rails) && Rails.respond_to?(:logger)
25
48
  Rails.logger
26
49
  else
27
- Logger.new(STDOUT, level: :info)
50
+ Logger.new($stdout, level: :info)
28
51
  end
29
52
  end
30
53
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ class ConnectionHandler
5
+ def initialize(connection_factory:, statement_timeout:)
6
+ @connection_factory = connection_factory
7
+ @statement_timeout = statement_timeout
8
+ cache_facts
9
+ end
10
+
11
+ def with_connection
12
+ connection = connection_factory.call
13
+ if supports_timeout?
14
+ connection.execute(timeout_set_command)
15
+ yield connection
16
+ connection.execute(reset_timeout_command)
17
+ else
18
+ yield connection
19
+ end
20
+ ensure
21
+ connection&.close
22
+ end
23
+
24
+ def supports_timeout?
25
+ @supports_timeout
26
+ end
27
+
28
+ def supports_copy?
29
+ @supports_copy
30
+ end
31
+
32
+ def timeout_set_command
33
+ "SET statement_timeout = \"#{statement_timeout}\""
34
+ end
35
+
36
+ def reset_timeout_command
37
+ "RESET statement_timeout"
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :connection_factory, :statement_timeout
43
+
44
+ def cache_facts
45
+ connection = connection_factory.call
46
+ @supports_timeout = connection.adapter_name.downcase.to_sym == :postgresql
47
+ @supports_copy = connection.raw_connection.respond_to?(:copy_data)
48
+ ensure
49
+ connection&.close
50
+ end
51
+ end
52
+ end
@@ -2,15 +2,26 @@
2
2
 
3
3
  module ActiveRecordDataLoader
4
4
  class CopyStrategy
5
- def initialize(data_generator)
5
+ def initialize(data_generator, file_adapter)
6
6
  @data_generator = data_generator
7
+ @file_adapter = file_adapter
7
8
  end
8
9
 
9
10
  def load_batch(row_numbers, connection)
10
- csv_data = csv_data_batch(row_numbers, connection)
11
-
12
- raw_connection = connection.raw_connection
13
- raw_connection.copy_data(copy_command(connection)) { raw_connection.put_copy_data(csv_data) }
11
+ data = csv_rows(row_numbers, connection)
12
+ copy(
13
+ connection: connection,
14
+ table: table_name_for_copy(connection),
15
+ columns: columns_for_copy(connection),
16
+ data: data,
17
+ row_numbers: row_numbers
18
+ )
19
+ file_adapter.copy(
20
+ table: table_name_for_copy(connection),
21
+ columns: columns_for_copy(connection),
22
+ data: data,
23
+ row_numbers: row_numbers
24
+ )
14
25
  end
15
26
 
16
27
  def table_name
@@ -23,29 +34,32 @@ module ActiveRecordDataLoader
23
34
 
24
35
  private
25
36
 
26
- attr_reader :data_generator
37
+ attr_reader :data_generator, :file_adapter
27
38
 
28
- def csv_data_batch(row_numbers, connection)
29
- row_numbers.map do |i|
30
- data_generator.generate_row(i).map { |d| quote_data(d, connection) }.join(",")
31
- end.join("\n")
32
- end
33
-
34
- def copy_command(connection)
35
- @copy_command ||= begin
36
- quoted_table_name = connection.quote_table_name(data_generator.table)
37
- columns = data_generator
38
- .column_list
39
- .map { |c| connection.quote_column_name(c) }
40
- .join(", ")
41
-
42
- <<~SQL
43
- COPY #{quoted_table_name} (#{columns})
44
- FROM STDIN WITH (FORMAT CSV)
45
- SQL
39
+ def copy(connection:, table:, columns:, data:, row_numbers:)
40
+ raw_connection = connection.raw_connection
41
+ raw_connection.copy_data("COPY #{table} (#{columns}) FROM STDIN WITH (FORMAT CSV)") do
42
+ raw_connection.put_copy_data(data.join("\n"))
46
43
  end
47
44
  end
48
45
 
46
+ def csv_rows(row_numbers, connection)
47
+ row_numbers.map do |i|
48
+ data_generator.generate_row(i)&.map { |d| quote_data(d, connection) }&.join(",")
49
+ end.compact
50
+ end
51
+
52
+ def table_name_for_copy(connection)
53
+ @table_name_for_copy ||= connection.quote_table_name(data_generator.table)
54
+ end
55
+
56
+ def columns_for_copy(connection)
57
+ @columns_for_copy ||= data_generator
58
+ .column_list
59
+ .map { |c| connection.quote_column_name(c) }
60
+ .join(", ")
61
+ end
62
+
49
63
  def quote_data(data, connection)
50
64
  return if data.nil?
51
65
 
@@ -13,16 +13,24 @@ module ActiveRecordDataLoader
13
13
 
14
14
  def adapter
15
15
  @adapter ||=
16
- if Gem.loaded_specs.key?("ffaker")
17
- require "ffaker"
16
+ if can_use?("ffaker", "2.1.0")
18
17
  FFakerGemAdapter.new
19
- elsif Gem.loaded_specs.key?("faker")
20
- require "faker"
18
+ elsif can_use?("faker", "1.9.3")
21
19
  FakerGemAdapter.new
22
20
  else
23
21
  NoGemAdapter.new
24
22
  end
25
23
  end
24
+
25
+ def can_use?(gem, min_version)
26
+ gemspec = Gem.loaded_specs[gem]
27
+ return false unless gemspec.present? && gemspec.version >= Gem::Version.new(min_version)
28
+
29
+ require gem
30
+ true
31
+ rescue LoadError
32
+ false
33
+ end
26
34
  end
27
35
 
28
36
  class FFakerGemAdapter
@@ -3,13 +3,16 @@
3
3
  module ActiveRecordDataLoader
4
4
  module Dsl
5
5
  class Model
6
- attr_reader :klass, :columns, :row_count, :polymorphic_associations, :belongs_to_associations
6
+ attr_reader :klass, :columns, :row_count, :polymorphic_associations, :belongs_to_associations,
7
+ :raise_on_duplicates_flag
7
8
 
8
9
  def initialize(klass:, configuration:)
9
10
  @klass = klass
10
11
  @columns = {}
11
12
  @row_count = configuration.default_row_count
12
13
  @batch_size = configuration.default_batch_size
14
+ @raise_on_duplicates_flag = configuration.raise_on_duplicates
15
+ @max_duplicate_retries = configuration.max_duplicate_retries
13
16
  @polymorphic_associations = []
14
17
  @belongs_to_associations = []
15
18
  end
@@ -22,6 +25,20 @@ module ActiveRecordDataLoader
22
25
  @batch_size = (size || @batch_size)
23
26
  end
24
27
 
28
+ def raise_on_duplicates
29
+ @raise_on_duplicates_flag = true
30
+ end
31
+
32
+ def do_not_raise_on_duplicates
33
+ @raise_on_duplicates_flag = false
34
+ end
35
+
36
+ def max_duplicate_retries(retries = nil)
37
+ return @max_duplicate_retries if retries.nil?
38
+
39
+ @max_duplicate_retries = retries
40
+ end
41
+
25
42
  def column(name, func)
26
43
  @columns[name.to_sym] = func
27
44
  end
@@ -32,7 +49,7 @@ module ActiveRecordDataLoader
32
49
  ).tap { |a| block.call(a) }
33
50
  end
34
51
 
35
- def belongs_to(assoc_name, eligible_set:)
52
+ def belongs_to(assoc_name, eligible_set: nil)
36
53
  @belongs_to_associations << BelongsToAssociation.new(@klass, assoc_name, eligible_set)
37
54
  end
38
55
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ class DuplicateKeyError < StandardError; end
5
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ class FileOutputAdapter
5
+ def self.with_output_options(options)
6
+ adapter = new(options)
7
+ pre_command = options[:pre_command]
8
+ adapter.write_command(pre_command) if pre_command
9
+ yield adapter
10
+ post_command = options[:post_command]
11
+ adapter.write_command(post_command) if post_command
12
+ end
13
+
14
+ def initialize(options)
15
+ @filename = options.fetch(:filename, "active_record_data_loader_script.sql")
16
+ @file_basename = File.basename(@filename, File.extname(@filename))
17
+ @path = File.expand_path(File.dirname(@filename))
18
+ File.open(@filename, File::TRUNC) if File.exist?(@filename)
19
+ end
20
+
21
+ def copy(table:, columns:, data:, row_numbers:)
22
+ data_filename = data_filename(table, row_numbers)
23
+ File.open(data_filename, "w") { |f| f.puts(data) }
24
+ File.open(filename, "a") do |file|
25
+ file.puts("\\COPY #{table} (#{columns}) FROM '#{data_filename}' WITH (FORMAT CSV);")
26
+ end
27
+ end
28
+
29
+ def insert(command)
30
+ write_command(command)
31
+ end
32
+
33
+ def write_command(command)
34
+ File.open(filename, "a") { |f| f.puts("#{command.gsub("\n", ' ')};") }
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :filename, :path, :file_basename
40
+
41
+ def data_filename(table, row_numbers)
42
+ File.join(
43
+ path,
44
+ "#{file_basename}_#{table.gsub(/"/, '')}_rows_#{row_numbers[0]}_to_#{row_numbers[-1]}.csv"
45
+ )
46
+ end
47
+ end
48
+ end
@@ -1,98 +1,82 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "benchmark"
4
-
5
3
  module ActiveRecordDataLoader
6
4
  class Loader
7
- class << self
8
- def load_data(
9
- data_generator:,
10
- total_rows:,
11
- batch_size:,
12
- configuration:
13
- )
14
- new(
15
- logger: configuration.logger,
16
- statement_timeout: configuration.statement_timeout,
17
- strategy: strategy_class(configuration.connection_factory).new(data_generator),
18
- connection_factory: configuration.connection_factory
19
- ).load_data(batch_size, total_rows)
20
- end
21
-
22
- private
23
-
24
- def strategy_class(connection_factory)
25
- if connection_factory.call.raw_connection.respond_to?(:copy_data)
26
- ActiveRecordDataLoader::CopyStrategy
27
- else
28
- ActiveRecordDataLoader::BulkInsertStrategy
29
- end
30
- end
5
+ def initialize(configuration, definition)
6
+ @configuration = configuration
7
+ @definition = definition
31
8
  end
32
9
 
33
- def initialize(logger:, statement_timeout:, strategy:, connection_factory:)
34
- @logger = logger
35
- @strategy = strategy
36
- @statement_timeout = statement_timeout
37
- @connection_factory = connection_factory
38
- end
10
+ def load_data
11
+ ActiveRecordDataLoader::ActiveRecord::PerRowValueCache.clear
39
12
 
40
- def load_data(batch_size, total_rows)
41
- batch_count = (total_rows / batch_size.to_f).ceil
42
-
43
- logger.info(
44
- "[ActiveRecordDataLoader] "\
45
- "Loading #{total_rows} row(s) into '#{strategy.table_name}' via #{strategy.name}. "\
46
- "#{batch_size} row(s) per batch, #{batch_count} batch(es)."
47
- )
48
- total_time = Benchmark.realtime do
49
- load_in_batches(batch_size, total_rows, batch_count)
13
+ file_adapter_class.with_output_options(file_adapter_options) do |file_adapter|
14
+ definition.models.map { |m| load_model(m, file_adapter) }
50
15
  end
51
- logger.info(
52
- "[ActiveRecordDataLoader] "\
53
- "Completed loading #{total_rows} row(s) into '#{strategy.table_name}' "\
54
- "in #{total_time} seconds."
55
- )
56
16
  end
57
17
 
58
18
  private
59
19
 
60
- attr_reader :strategy, :statement_timeout, :logger, :connection_factory
20
+ attr_reader :definition, :configuration
61
21
 
62
- def load_in_batches(batch_size, total_rows, batch_count)
63
- with_connection do |connection|
64
- total_rows.times.each_slice(batch_size).with_index do |row_numbers, i|
65
- time = Benchmark.realtime { strategy.load_batch(row_numbers, connection) }
22
+ def load_model(model, file_adapter)
23
+ ActiveRecordDataLoader::TableLoader.load_data(
24
+ batch_size: model.batch_size,
25
+ total_rows: model.row_count,
26
+ connection_handler: connection_handler,
27
+ strategy: strategy_class.new(generator(model), file_adapter),
28
+ logger: configuration.logger
29
+ )
30
+ end
66
31
 
67
- logger.debug(
68
- "[ActiveRecordDataLoader] "\
69
- "Completed batch #{i + 1}/#{batch_count}, #{row_numbers.count} row(s) in #{time} seconds"
70
- )
71
- end
72
- end
32
+ def generator(model)
33
+ ActiveRecordDataLoader::ActiveRecord::ModelDataGenerator.new(
34
+ model: model.klass,
35
+ column_settings: model.columns,
36
+ polymorphic_settings: model.polymorphic_associations,
37
+ belongs_to_settings: model.belongs_to_associations,
38
+ connection_factory: configuration.connection_factory,
39
+ raise_on_duplicates: model.raise_on_duplicates_flag,
40
+ max_duplicate_retries: model.max_duplicate_retries,
41
+ logger: configuration.logger
42
+ )
73
43
  end
74
44
 
75
- def with_connection
76
- if connection.adapter_name.downcase.to_sym == :postgresql
77
- original_timeout = retrieve_statement_timeout
78
- update_statement_timeout(statement_timeout)
79
- yield connection
80
- update_statement_timeout(original_timeout)
45
+ def file_adapter_class
46
+ if configuration.output.present?
47
+ ActiveRecordDataLoader::FileOutputAdapter
81
48
  else
82
- yield connection
49
+ ActiveRecordDataLoader::NullOutputAdapter
83
50
  end
84
51
  end
85
52
 
86
- def retrieve_statement_timeout
87
- connection.execute("SHOW statement_timeout").first["statement_timeout"]
53
+ def file_adapter_options
54
+ timeout_commands =
55
+ if connection_handler.supports_timeout?
56
+ {
57
+ pre_command: connection_handler.timeout_set_command,
58
+ post_command: connection_handler.reset_timeout_command,
59
+ }
60
+ else
61
+ {}
62
+ end
63
+
64
+ timeout_commands.merge(filename: configuration.output)
88
65
  end
89
66
 
90
- def update_statement_timeout(timeout)
91
- connection.execute("SET statement_timeout = \"#{timeout}\"")
67
+ def strategy_class
68
+ @strategy_class ||= if connection_handler.supports_copy?
69
+ ActiveRecordDataLoader::CopyStrategy
70
+ else
71
+ ActiveRecordDataLoader::BulkInsertStrategy
72
+ end
92
73
  end
93
74
 
94
- def connection
95
- connection_factory.call
75
+ def connection_handler
76
+ @connection_handler ||= ActiveRecordDataLoader::ConnectionHandler.new(
77
+ connection_factory: configuration.connection_factory,
78
+ statement_timeout: configuration.statement_timeout
79
+ )
96
80
  end
97
81
  end
98
82
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecordDataLoader
4
+ class NullOutputAdapter
5
+ def self.with_output_options(_options)
6
+ yield new
7
+ end
8
+
9
+ def copy(table:, columns:, data:, row_numbers:); end
10
+
11
+ def insert(command); end
12
+
13
+ def write_command(command); end
14
+ end
15
+ end