activerecord-import 0.22.0 → 1.4.1

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yaml +107 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +74 -8
  5. data/Brewfile +3 -1
  6. data/CHANGELOG.md +235 -3
  7. data/Gemfile +22 -15
  8. data/LICENSE +21 -56
  9. data/README.markdown +574 -22
  10. data/Rakefile +4 -1
  11. data/activerecord-import.gemspec +6 -5
  12. data/benchmarks/benchmark.rb +7 -1
  13. data/benchmarks/lib/base.rb +2 -0
  14. data/benchmarks/lib/cli_parser.rb +3 -1
  15. data/benchmarks/lib/float.rb +2 -0
  16. data/benchmarks/lib/mysql2_benchmark.rb +2 -0
  17. data/benchmarks/lib/output_to_csv.rb +2 -0
  18. data/benchmarks/lib/output_to_html.rb +4 -2
  19. data/benchmarks/models/test_innodb.rb +2 -0
  20. data/benchmarks/models/test_memory.rb +2 -0
  21. data/benchmarks/models/test_myisam.rb +2 -0
  22. data/benchmarks/schema/{mysql_schema.rb → mysql2_schema.rb} +2 -0
  23. data/gemfiles/4.2.gemfile +2 -0
  24. data/gemfiles/5.0.gemfile +2 -0
  25. data/gemfiles/5.1.gemfile +2 -0
  26. data/gemfiles/5.2.gemfile +4 -0
  27. data/gemfiles/6.0.gemfile +4 -0
  28. data/gemfiles/6.1.gemfile +4 -0
  29. data/gemfiles/7.0.gemfile +4 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +2 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -4
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +2 -0
  33. data/lib/activerecord-import/active_record/adapters/jdbcsqlite3_adapter.rb +2 -0
  34. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +2 -0
  35. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +2 -0
  36. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +2 -0
  37. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +2 -0
  38. data/lib/activerecord-import/adapters/abstract_adapter.rb +10 -2
  39. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +2 -0
  40. data/lib/activerecord-import/adapters/mysql2_adapter.rb +2 -0
  41. data/lib/activerecord-import/adapters/mysql_adapter.rb +19 -11
  42. data/lib/activerecord-import/adapters/postgresql_adapter.rb +56 -37
  43. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +128 -9
  44. data/lib/activerecord-import/base.rb +12 -2
  45. data/lib/activerecord-import/import.rb +300 -136
  46. data/lib/activerecord-import/mysql2.rb +2 -0
  47. data/lib/activerecord-import/postgresql.rb +2 -0
  48. data/lib/activerecord-import/sqlite3.rb +2 -0
  49. data/lib/activerecord-import/synchronize.rb +4 -2
  50. data/lib/activerecord-import/value_sets_parser.rb +4 -0
  51. data/lib/activerecord-import/version.rb +3 -1
  52. data/lib/activerecord-import.rb +4 -1
  53. data/test/adapters/jdbcmysql.rb +2 -0
  54. data/test/adapters/jdbcpostgresql.rb +2 -0
  55. data/test/adapters/jdbcsqlite3.rb +2 -0
  56. data/test/adapters/makara_postgis.rb +3 -0
  57. data/test/adapters/mysql2.rb +2 -0
  58. data/test/adapters/mysql2_makara.rb +2 -0
  59. data/test/adapters/mysql2spatial.rb +2 -0
  60. data/test/adapters/postgis.rb +2 -0
  61. data/test/adapters/postgresql.rb +2 -0
  62. data/test/adapters/postgresql_makara.rb +2 -0
  63. data/test/adapters/seamless_database_pool.rb +2 -0
  64. data/test/adapters/spatialite.rb +2 -0
  65. data/test/adapters/sqlite3.rb +2 -0
  66. data/test/{travis → github}/database.yml +3 -1
  67. data/test/import_test.rb +159 -8
  68. data/test/jdbcmysql/import_test.rb +2 -0
  69. data/test/jdbcpostgresql/import_test.rb +2 -0
  70. data/test/jdbcsqlite3/import_test.rb +2 -0
  71. data/test/makara_postgis/import_test.rb +10 -0
  72. data/test/models/account.rb +5 -0
  73. data/test/models/alarm.rb +2 -0
  74. data/test/models/animal.rb +8 -0
  75. data/test/models/bike_maker.rb +9 -0
  76. data/test/models/book.rb +2 -0
  77. data/test/models/car.rb +2 -0
  78. data/test/models/card.rb +5 -0
  79. data/test/models/chapter.rb +2 -0
  80. data/test/models/customer.rb +8 -0
  81. data/test/models/deck.rb +8 -0
  82. data/test/models/dictionary.rb +2 -0
  83. data/test/models/discount.rb +2 -0
  84. data/test/models/end_note.rb +2 -0
  85. data/test/models/group.rb +2 -0
  86. data/test/models/order.rb +8 -0
  87. data/test/models/playing_card.rb +4 -0
  88. data/test/models/promotion.rb +2 -0
  89. data/test/models/question.rb +2 -0
  90. data/test/models/rule.rb +2 -0
  91. data/test/models/tag.rb +3 -0
  92. data/test/models/tag_alias.rb +5 -0
  93. data/test/models/topic.rb +2 -0
  94. data/test/models/user.rb +5 -0
  95. data/test/models/user_token.rb +6 -0
  96. data/test/models/vendor.rb +2 -0
  97. data/test/models/widget.rb +2 -0
  98. data/test/mysql2/import_test.rb +2 -0
  99. data/test/mysql2_makara/import_test.rb +2 -0
  100. data/test/mysqlspatial2/import_test.rb +2 -0
  101. data/test/postgis/import_test.rb +2 -0
  102. data/test/postgresql/import_test.rb +2 -0
  103. data/test/schema/generic_schema.rb +53 -0
  104. data/test/schema/jdbcpostgresql_schema.rb +2 -0
  105. data/test/schema/mysql2_schema.rb +21 -0
  106. data/test/schema/postgis_schema.rb +2 -0
  107. data/test/schema/postgresql_schema.rb +18 -0
  108. data/test/schema/sqlite3_schema.rb +15 -0
  109. data/test/schema/version.rb +2 -0
  110. data/test/sqlite3/import_test.rb +2 -0
  111. data/test/support/active_support/test_case_extensions.rb +2 -0
  112. data/test/support/assertions.rb +2 -0
  113. data/test/support/factories.rb +10 -8
  114. data/test/support/generate.rb +10 -8
  115. data/test/support/mysql/import_examples.rb +14 -1
  116. data/test/support/postgresql/import_examples.rb +140 -3
  117. data/test/support/shared_examples/on_duplicate_key_ignore.rb +2 -0
  118. data/test/support/shared_examples/on_duplicate_key_update.rb +263 -0
  119. data/test/support/shared_examples/recursive_import.rb +76 -4
  120. data/test/support/sqlite3/import_examples.rb +191 -26
  121. data/test/synchronize_test.rb +2 -0
  122. data/test/test_helper.rb +36 -3
  123. data/test/value_sets_bytes_parser_test.rb +2 -0
  124. data/test/value_sets_records_parser_test.rb +2 -0
  125. metadata +46 -18
  126. data/.travis.yml +0 -61
  127. data/gemfiles/3.2.gemfile +0 -2
  128. data/gemfiles/4.0.gemfile +0 -2
  129. data/gemfiles/4.1.gemfile +0 -2
  130. data/test/schema/mysql_schema.rb +0 -16
@@ -1,18 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActiveRecord::Import::SQLite3Adapter
2
4
  include ActiveRecord::Import::ImportSupport
5
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
3
6
 
4
- MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
7
+ MIN_VERSION_FOR_IMPORT = "3.7.11"
8
+ MIN_VERSION_FOR_UPSERT = "3.24.0"
5
9
  SQLITE_LIMIT_COMPOUND_SELECT = 500
6
10
 
7
11
  # Override our conformance to ActiveRecord::Import::ImportSupport interface
8
12
  # to ensure that we only support import in supported version of SQLite.
9
13
  # Which INSERT statements with multiple value sets was introduced in 3.7.11.
10
- def supports_import?(current_version = sqlite_version)
11
- if current_version >= MIN_VERSION_FOR_IMPORT
12
- true
13
- else
14
- false
15
- end
14
+ def supports_import?
15
+ database_version >= MIN_VERSION_FOR_IMPORT
16
+ end
17
+
18
+ def supports_on_duplicate_key_update?
19
+ database_version >= MIN_VERSION_FOR_UPSERT
16
20
  end
17
21
 
18
22
  # +sql+ can be a single string or an array. If it is an array all
@@ -40,16 +44,131 @@ module ActiveRecord::Import::SQLite3Adapter
40
44
  ActiveRecord::Import::Result.new([], number_of_inserts, [], [])
41
45
  end
42
46
 
43
- def pre_sql_statements( options)
47
+ def pre_sql_statements( options )
44
48
  sql = []
45
49
  # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
46
- if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:recursive]
50
+ if !supports_on_duplicate_key_update? && (options[:ignore] || options[:on_duplicate_key_ignore])
47
51
  sql << "OR IGNORE"
48
52
  end
49
53
  sql + super
50
54
  end
51
55
 
56
+ def post_sql_statements( table_name, options ) # :nodoc:
57
+ sql = []
58
+
59
+ if supports_on_duplicate_key_update?
60
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
61
+ if (options[:ignore] || options[:on_duplicate_key_ignore]) && !options[:on_duplicate_key_update]
62
+ sql << sql_for_on_duplicate_key_ignore( options[:on_duplicate_key_ignore] )
63
+ end
64
+ end
65
+
66
+ sql + super
67
+ end
68
+
52
69
  def next_value_for_sequence(sequence_name)
53
70
  %{nextval('#{sequence_name}')}
54
71
  end
72
+
73
+ # Add a column to be updated on duplicate key update
74
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
75
+ arg = options[:on_duplicate_key_update]
76
+ if arg.is_a?( Hash )
77
+ columns = arg.fetch( :columns ) { arg[:columns] = [] }
78
+ case columns
79
+ when Array then columns << column.to_sym unless columns.include?( column.to_sym )
80
+ when Hash then columns[column.to_sym] = column.to_sym
81
+ end
82
+ elsif arg.is_a?( Array )
83
+ arg << column.to_sym unless arg.include?( column.to_sym )
84
+ end
85
+ end
86
+
87
+ # Returns a generated ON CONFLICT DO NOTHING statement given the passed
88
+ # in +args+.
89
+ def sql_for_on_duplicate_key_ignore( *args ) # :nodoc:
90
+ arg = args.first
91
+ conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
92
+ " ON CONFLICT #{conflict_target}DO NOTHING"
93
+ end
94
+
95
+ # Returns a generated ON CONFLICT DO UPDATE statement given the passed
96
+ # in +args+.
97
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
98
+ arg, primary_key, locking_column = args
99
+ arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
100
+ return unless arg.is_a?( Hash )
101
+
102
+ sql = ' ON CONFLICT '.dup
103
+ conflict_target = sql_for_conflict_target( arg )
104
+
105
+ columns = arg.fetch( :columns, [] )
106
+ condition = arg[:condition]
107
+ if columns.respond_to?( :empty? ) && columns.empty?
108
+ return sql << "#{conflict_target}DO NOTHING"
109
+ end
110
+
111
+ conflict_target ||= sql_for_default_conflict_target( primary_key )
112
+ unless conflict_target
113
+ raise ArgumentError, 'Expected :conflict_target to be specified'
114
+ end
115
+
116
+ sql << "#{conflict_target}DO UPDATE SET "
117
+ if columns.is_a?( Array )
118
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, locking_column, columns )
119
+ elsif columns.is_a?( Hash )
120
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, columns )
121
+ elsif columns.is_a?( String )
122
+ sql << columns
123
+ else
124
+ raise ArgumentError, 'Expected :columns to be an Array or Hash'
125
+ end
126
+
127
+ sql << " WHERE #{condition}" if condition.present?
128
+
129
+ sql
130
+ end
131
+
132
+ def sql_for_on_duplicate_key_update_as_array( table_name, locking_column, arr ) # :nodoc:
133
+ results = arr.map do |column|
134
+ qc = quote_column_name( column )
135
+ "#{qc}=EXCLUDED.#{qc}"
136
+ end
137
+ increment_locking_column!(table_name, results, locking_column)
138
+ results.join( ',' )
139
+ end
140
+
141
+ def sql_for_on_duplicate_key_update_as_hash( table_name, locking_column, hsh ) # :nodoc:
142
+ results = hsh.map do |column1, column2|
143
+ qc1 = quote_column_name( column1 )
144
+ qc2 = quote_column_name( column2 )
145
+ "#{qc1}=EXCLUDED.#{qc2}"
146
+ end
147
+ increment_locking_column!(table_name, results, locking_column)
148
+ results.join( ',' )
149
+ end
150
+
151
+ def sql_for_conflict_target( args = {} )
152
+ conflict_target = args[:conflict_target]
153
+ index_predicate = args[:index_predicate]
154
+ if conflict_target.present?
155
+ sql = '(' + Array( conflict_target ).reject( &:blank? ).join( ', ' ) + ') '
156
+ sql += "WHERE #{index_predicate} " if index_predicate
157
+ sql
158
+ end
159
+ end
160
+
161
+ def sql_for_default_conflict_target( primary_key )
162
+ conflict_target = Array(primary_key).join(', ')
163
+ "(#{conflict_target}) " if conflict_target.present?
164
+ end
165
+
166
+ # Return true if the statement is a duplicate key record error
167
+ def duplicate_key_update_error?(exception) # :nodoc:
168
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
169
+ end
170
+
171
+ def database_version
172
+ defined?(sqlite_version) ? sqlite_version : super
173
+ end
55
174
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "pathname"
2
4
  require "active_record"
3
5
  require "active_record/version"
4
6
 
5
7
  module ActiveRecord::Import
6
- ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
8
+ ADAPTER_PATH = "activerecord-import/active_record/adapters"
7
9
 
8
10
  def self.base_adapter(adapter)
9
11
  case adapter
@@ -11,7 +13,9 @@ module ActiveRecord::Import
11
13
  when 'mysql2spatial' then 'mysql2'
12
14
  when 'spatialite' then 'sqlite3'
13
15
  when 'postgresql_makara' then 'postgresql'
16
+ when 'makara_postgis' then 'postgresql'
14
17
  when 'postgis' then 'postgresql'
18
+ when 'cockroachdb' then 'postgresql'
15
19
  else adapter
16
20
  end
17
21
  end
@@ -25,7 +29,13 @@ module ActiveRecord::Import
25
29
 
26
30
  # Loads the import functionality for the passed in ActiveRecord connection
27
31
  def self.load_from_connection_pool(connection_pool)
28
- require_adapter connection_pool.spec.config[:adapter]
32
+ adapter =
33
+ if connection_pool.respond_to?(:db_config) # ActiveRecord >= 6.1
34
+ connection_pool.db_config.adapter
35
+ else
36
+ connection_pool.spec.config[:adapter]
37
+ end
38
+ require_adapter adapter
29
39
  end
30
40
  end
31
41