activerecord-import 0.12.0 → 0.13.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 +49 -0
- data/.rubocop_todo.yml +36 -0
- data/.travis.yml +31 -7
- data/CHANGELOG.md +19 -0
- data/Gemfile +5 -2
- data/README.markdown +6 -1
- data/Rakefile +5 -2
- data/activerecord-import.gemspec +1 -1
- data/benchmarks/benchmark.rb +67 -68
- data/benchmarks/lib/base.rb +136 -137
- data/benchmarks/lib/cli_parser.rb +106 -107
- data/benchmarks/lib/mysql2_benchmark.rb +19 -21
- data/benchmarks/lib/output_to_csv.rb +2 -1
- data/benchmarks/lib/output_to_html.rb +8 -13
- data/benchmarks/schema/mysql_schema.rb +8 -8
- data/gemfiles/4.0.gemfile +1 -1
- data/gemfiles/4.1.gemfile +1 -1
- data/gemfiles/4.2.gemfile +1 -1
- data/gemfiles/5.0.gemfile +1 -1
- data/lib/activerecord-import.rb +2 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +0 -1
- data/lib/activerecord-import/adapters/abstract_adapter.rb +9 -9
- data/lib/activerecord-import/adapters/mysql_adapter.rb +17 -17
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +20 -22
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +9 -9
- data/lib/activerecord-import/base.rb +3 -3
- data/lib/activerecord-import/import.rb +152 -131
- data/lib/activerecord-import/synchronize.rb +20 -20
- data/lib/activerecord-import/value_sets_parser.rb +7 -6
- data/lib/activerecord-import/version.rb +1 -1
- data/test/adapters/mysql2spatial.rb +1 -1
- data/test/adapters/postgis.rb +1 -1
- data/test/adapters/postgresql.rb +1 -1
- data/test/adapters/spatialite.rb +1 -1
- data/test/adapters/sqlite3.rb +1 -1
- data/test/import_test.rb +121 -70
- data/test/models/book.rb +5 -6
- data/test/models/chapter.rb +2 -2
- data/test/models/discount.rb +3 -0
- data/test/models/end_note.rb +2 -2
- data/test/models/promotion.rb +1 -1
- data/test/models/question.rb +1 -1
- data/test/models/rule.rb +2 -2
- data/test/models/topic.rb +3 -3
- data/test/models/widget.rb +1 -1
- data/test/postgis/import_test.rb +1 -1
- data/test/schema/generic_schema.rb +100 -96
- data/test/schema/mysql_schema.rb +5 -7
- data/test/sqlite3/import_test.rb +0 -2
- data/test/support/active_support/test_case_extensions.rb +12 -15
- data/test/support/assertions.rb +1 -1
- data/test/support/factories.rb +15 -16
- data/test/support/generate.rb +4 -4
- data/test/support/mysql/import_examples.rb +21 -21
- data/test/support/postgresql/import_examples.rb +83 -55
- data/test/support/shared_examples/on_duplicate_key_update.rb +23 -23
- data/test/synchronize_test.rb +2 -2
- data/test/test_helper.rb +6 -8
- data/test/value_sets_bytes_parser_test.rb +14 -17
- data/test/value_sets_records_parser_test.rb +6 -6
- metadata +7 -4
- data/test/travis/build.sh +0 -34
@@ -2,15 +2,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
2
2
|
include ActiveRecord::Import::ImportSupport
|
3
3
|
include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
|
4
4
|
|
5
|
-
MIN_VERSION_FOR_UPSERT =
|
5
|
+
MIN_VERSION_FOR_UPSERT = 90_500
|
6
6
|
|
7
7
|
def insert_many( sql, values, *args ) # :nodoc:
|
8
8
|
number_of_inserts = 1
|
9
9
|
|
10
|
-
base_sql,post_sql = if sql.is_a?( String )
|
11
|
-
[
|
10
|
+
base_sql, post_sql = if sql.is_a?( String )
|
11
|
+
[sql, '']
|
12
12
|
elsif sql.is_a?( Array )
|
13
|
-
[
|
13
|
+
[sql.shift, sql.join( ' ' )]
|
14
14
|
end
|
15
15
|
|
16
16
|
sql2insert = base_sql + values.join( ',' ) + post_sql
|
@@ -18,7 +18,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
18
18
|
|
19
19
|
ActiveRecord::Base.connection.query_cache.clear
|
20
20
|
|
21
|
-
[number_of_inserts,ids]
|
21
|
+
[number_of_inserts, ids]
|
22
22
|
end
|
23
23
|
|
24
24
|
def next_value_for_sequence(sequence_name)
|
@@ -26,15 +26,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def post_sql_statements( table_name, options ) # :nodoc:
|
29
|
-
|
30
|
-
super(table_name, options) << ("RETURNING #{options[:primary_key]}")
|
31
|
-
else
|
29
|
+
if options[:primary_key].blank?
|
32
30
|
super(table_name, options)
|
31
|
+
else
|
32
|
+
super(table_name, options) << "RETURNING #{options[:primary_key]}"
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
# Add a column to be updated on duplicate key update
|
37
|
-
def add_column_for_on_duplicate_key_update( column, options={} ) # :nodoc:
|
37
|
+
def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
|
38
38
|
arg = options[:on_duplicate_key_update]
|
39
39
|
if arg.is_a?( Hash )
|
40
40
|
columns = arg.fetch( :columns ) { arg[:columns] = [] }
|
@@ -51,9 +51,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
51
51
|
# in +args+.
|
52
52
|
def sql_for_on_duplicate_key_ignore( table_name, *args ) # :nodoc:
|
53
53
|
arg = args.first
|
54
|
-
if arg.is_a?( Hash )
|
55
|
-
conflict_target = sql_for_conflict_target( arg )
|
56
|
-
end
|
54
|
+
conflict_target = sql_for_conflict_target( arg ) if arg.is_a?( Hash )
|
57
55
|
" ON CONFLICT #{conflict_target}DO NOTHING"
|
58
56
|
end
|
59
57
|
|
@@ -61,9 +59,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
61
59
|
# in +args+.
|
62
60
|
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
63
61
|
arg = args.first
|
64
|
-
if arg.is_a?( Array ) || arg.is_a?( String )
|
65
|
-
arg = { :columns => arg }
|
66
|
-
end
|
62
|
+
arg = { columns: arg } if arg.is_a?( Array ) || arg.is_a?( String )
|
67
63
|
return unless arg.is_a?( Hash )
|
68
64
|
|
69
65
|
sql = " ON CONFLICT "
|
@@ -92,7 +88,7 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
92
88
|
sql
|
93
89
|
end
|
94
90
|
|
95
|
-
def sql_for_on_duplicate_key_update_as_array( table_name, arr )
|
91
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
|
96
92
|
results = arr.map do |column|
|
97
93
|
qc = quote_column_name( column )
|
98
94
|
"#{qc}=EXCLUDED.#{qc}"
|
@@ -109,10 +105,12 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
109
105
|
results.join( ',' )
|
110
106
|
end
|
111
107
|
|
112
|
-
def sql_for_conflict_target( args={} )
|
113
|
-
|
108
|
+
def sql_for_conflict_target( args = {} )
|
109
|
+
constraint_name = args[:constraint_name]
|
110
|
+
conflict_target = args[:conflict_target]
|
111
|
+
if constraint_name
|
114
112
|
"ON CONSTRAINT #{constraint_name} "
|
115
|
-
elsif conflict_target
|
113
|
+
elsif conflict_target
|
116
114
|
'(' << Array( conflict_target ).join( ', ' ) << ') '
|
117
115
|
end
|
118
116
|
end
|
@@ -122,15 +120,15 @@ module ActiveRecord::Import::PostgreSQLAdapter
|
|
122
120
|
end
|
123
121
|
|
124
122
|
# Return true if the statement is a duplicate key record error
|
125
|
-
def duplicate_key_update_error?(exception)# :nodoc:
|
123
|
+
def duplicate_key_update_error?(exception) # :nodoc:
|
126
124
|
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('duplicate key')
|
127
125
|
end
|
128
126
|
|
129
|
-
def supports_on_duplicate_key_update?(current_version=
|
127
|
+
def supports_on_duplicate_key_update?(current_version = postgresql_version)
|
130
128
|
current_version >= MIN_VERSION_FOR_UPSERT
|
131
129
|
end
|
132
130
|
|
133
|
-
def supports_on_duplicate_key_ignore?(current_version=
|
131
|
+
def supports_on_duplicate_key_ignore?(current_version = postgresql_version)
|
134
132
|
supports_on_duplicate_key_update?(current_version)
|
135
133
|
end
|
136
134
|
|
@@ -1,13 +1,13 @@
|
|
1
1
|
module ActiveRecord::Import::SQLite3Adapter
|
2
2
|
include ActiveRecord::Import::ImportSupport
|
3
3
|
|
4
|
-
MIN_VERSION_FOR_IMPORT = "3.7.11"
|
4
|
+
MIN_VERSION_FOR_IMPORT = "3.7.11".freeze
|
5
5
|
SQLITE_LIMIT_COMPOUND_SELECT = 500
|
6
6
|
|
7
7
|
# Override our conformance to ActiveRecord::Import::ImportSupport interface
|
8
8
|
# to ensure that we only support import in supported version of SQLite.
|
9
9
|
# Which INSERT statements with multiple value sets was introduced in 3.7.11.
|
10
|
-
def supports_import?(current_version=
|
10
|
+
def supports_import?(current_version = sqlite_version)
|
11
11
|
if current_version >= MIN_VERSION_FOR_IMPORT
|
12
12
|
true
|
13
13
|
else
|
@@ -19,22 +19,22 @@ module ActiveRecord::Import::SQLite3Adapter
|
|
19
19
|
# elements that are in position >= 1 will be appended to the final SQL.
|
20
20
|
def insert_many(sql, values, *args) # :nodoc:
|
21
21
|
number_of_inserts = 0
|
22
|
-
base_sql,post_sql = if sql.is_a?( String )
|
23
|
-
[
|
22
|
+
base_sql, post_sql = if sql.is_a?( String )
|
23
|
+
[sql, '']
|
24
24
|
elsif sql.is_a?( Array )
|
25
|
-
[
|
25
|
+
[sql.shift, sql.join( ' ' )]
|
26
26
|
end
|
27
27
|
|
28
28
|
value_sets = ::ActiveRecord::Import::ValueSetsRecordsParser.parse(values,
|
29
|
-
:
|
29
|
+
max_records: SQLITE_LIMIT_COMPOUND_SELECT)
|
30
30
|
|
31
|
-
value_sets.each do |
|
31
|
+
value_sets.each do |value_set|
|
32
32
|
number_of_inserts += 1
|
33
|
-
sql2insert = base_sql +
|
33
|
+
sql2insert = base_sql + value_set.join( ',' ) + post_sql
|
34
34
|
insert( sql2insert, *args )
|
35
35
|
end
|
36
36
|
|
37
|
-
[number_of_inserts,[]]
|
37
|
+
[number_of_inserts, []]
|
38
38
|
end
|
39
39
|
|
40
40
|
def next_value_for_sequence(sequence_name)
|
@@ -3,7 +3,7 @@ require "active_record"
|
|
3
3
|
require "active_record/version"
|
4
4
|
|
5
5
|
module ActiveRecord::Import
|
6
|
-
|
6
|
+
ADAPTER_PATH = "activerecord-import/active_record/adapters".freeze
|
7
7
|
|
8
8
|
def self.base_adapter(adapter)
|
9
9
|
case adapter
|
@@ -16,9 +16,9 @@ module ActiveRecord::Import
|
|
16
16
|
|
17
17
|
# Loads the import functionality for a specific database adapter
|
18
18
|
def self.require_adapter(adapter)
|
19
|
-
require File.join(
|
19
|
+
require File.join(ADAPTER_PATH, "/abstract_adapter")
|
20
20
|
begin
|
21
|
-
require File.join(
|
21
|
+
require File.join(ADAPTER_PATH, "/#{base_adapter(adapter)}_adapter")
|
22
22
|
rescue LoadError
|
23
23
|
# fallback
|
24
24
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
require "ostruct"
|
2
2
|
|
3
|
-
module ActiveRecord::Import::ConnectionAdapters
|
3
|
+
module ActiveRecord::Import::ConnectionAdapters; end
|
4
4
|
|
5
5
|
module ActiveRecord::Import #:nodoc:
|
6
|
-
|
7
|
-
end
|
6
|
+
Result = Struct.new(:failed_instances, :num_inserts, :ids)
|
8
7
|
|
9
8
|
module ImportSupport #:nodoc:
|
10
9
|
def supports_import? #:nodoc:
|
@@ -39,15 +38,15 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
39
38
|
|
40
39
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
41
40
|
|
42
|
-
model_klass =
|
43
|
-
symbolized_foreign_key =
|
41
|
+
model_klass = reflection.klass
|
42
|
+
symbolized_foreign_key = reflection.foreign_key.to_sym
|
44
43
|
symbolized_column_names = model_klass.column_names.map(&:to_sym)
|
45
44
|
|
46
|
-
owner_primary_key =
|
47
|
-
owner_primary_key_value =
|
45
|
+
owner_primary_key = owner.class.primary_key
|
46
|
+
owner_primary_key_value = owner.send(owner_primary_key)
|
48
47
|
|
49
48
|
# assume array of model objects
|
50
|
-
if args.last.is_a?( Array )
|
49
|
+
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
51
50
|
if args.length == 2
|
52
51
|
models = args.last
|
53
52
|
column_names = args.first
|
@@ -56,54 +55,53 @@ class ActiveRecord::Associations::CollectionAssociation
|
|
56
55
|
column_names = symbolized_column_names
|
57
56
|
end
|
58
57
|
|
59
|
-
|
58
|
+
unless symbolized_column_names.include?(symbolized_foreign_key)
|
60
59
|
column_names << symbolized_foreign_key
|
61
60
|
end
|
62
61
|
|
63
62
|
models.each do |m|
|
64
|
-
m.
|
63
|
+
m.public_send "#{symbolized_foreign_key}=", owner_primary_key_value
|
65
64
|
end
|
66
65
|
|
67
66
|
return model_klass.import column_names, models, options
|
68
67
|
|
69
68
|
# supports empty array
|
70
|
-
elsif args.last.is_a?( Array )
|
69
|
+
elsif args.last.is_a?( Array ) && args.last.empty?
|
71
70
|
return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
|
72
71
|
|
73
72
|
# supports 2-element array and array
|
74
|
-
elsif args.size == 2
|
73
|
+
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
75
74
|
column_names, array_of_attributes = args
|
76
75
|
symbolized_column_names = column_names.map(&:to_s)
|
77
76
|
|
78
|
-
if
|
79
|
-
column_names << symbolized_foreign_key
|
80
|
-
array_of_attributes.each { |attrs| attrs << owner_primary_key_value }
|
81
|
-
else
|
77
|
+
if symbolized_column_names.include?(symbolized_foreign_key)
|
82
78
|
index = symbolized_column_names.index(symbolized_foreign_key)
|
83
79
|
array_of_attributes.each { |attrs| attrs[index] = owner_primary_key_value }
|
80
|
+
else
|
81
|
+
column_names << symbolized_foreign_key
|
82
|
+
array_of_attributes.each { |attrs| attrs << owner_primary_key_value }
|
84
83
|
end
|
85
84
|
|
86
85
|
return model_klass.import column_names, array_of_attributes, options
|
87
86
|
else
|
88
|
-
raise ArgumentError
|
87
|
+
raise ArgumentError, "Invalid arguments!"
|
89
88
|
end
|
90
89
|
end
|
91
90
|
end
|
92
91
|
|
93
92
|
class ActiveRecord::Base
|
94
93
|
class << self
|
95
|
-
|
96
94
|
# use tz as set in ActiveRecord::Base
|
97
95
|
tproc = lambda do
|
98
96
|
ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
99
97
|
end
|
100
98
|
|
101
99
|
AREXT_RAILS_COLUMNS = {
|
102
|
-
:
|
103
|
-
|
104
|
-
:
|
105
|
-
|
106
|
-
}
|
100
|
+
create: { "created_on" => tproc,
|
101
|
+
"created_at" => tproc },
|
102
|
+
update: { "updated_on" => tproc,
|
103
|
+
"updated_at" => tproc }
|
104
|
+
}.freeze
|
107
105
|
AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
|
108
106
|
|
109
107
|
# Returns true if the current database connection adapter
|
@@ -179,17 +177,19 @@ class ActiveRecord::Base
|
|
179
177
|
# existing model instances in memory with updates from the import.
|
180
178
|
# * +timestamps+ - true|false, tells import to not add timestamps
|
181
179
|
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
182
|
-
# * +recursive - true|false, tells import to import all has_many/has_one
|
180
|
+
# * +recursive+ - true|false, tells import to import all has_many/has_one
|
183
181
|
# associations if the adapter supports setting the primary keys of the
|
184
182
|
# newly imported objects.
|
183
|
+
# * +batch_size+ - an integer value to specify the max number of records to
|
184
|
+
# include per insert. Defaults to the total number of records to import.
|
185
185
|
#
|
186
186
|
# == Examples
|
187
187
|
# class BlogPost < ActiveRecord::Base ; end
|
188
188
|
#
|
189
189
|
# # Example using array of model objects
|
190
|
-
# posts = [ BlogPost.new :
|
191
|
-
# BlogPost.new :
|
192
|
-
# BlogPost.new :
|
190
|
+
# posts = [ BlogPost.new author_name: 'Zach Dennis', title: 'AREXT',
|
191
|
+
# BlogPost.new author_name: 'Zach Dennis', title: 'AREXT2',
|
192
|
+
# BlogPost.new author_name: 'Zach Dennis', title: 'AREXT3' ]
|
193
193
|
# BlogPost.import posts
|
194
194
|
#
|
195
195
|
# # Example using column_names and array_of_values
|
@@ -200,19 +200,19 @@ class ActiveRecord::Base
|
|
200
200
|
# # Example using column_names, array_of_value and options
|
201
201
|
# columns = [ :author_name, :title ]
|
202
202
|
# values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
|
203
|
-
# BlogPost.import( columns, values, :
|
203
|
+
# BlogPost.import( columns, values, validate: false )
|
204
204
|
#
|
205
205
|
# # Example synchronizing existing instances in memory
|
206
206
|
# post = BlogPost.where(author_name: 'zdennis').first
|
207
207
|
# puts post.author_name # => 'zdennis'
|
208
208
|
# columns = [ :author_name, :title ]
|
209
209
|
# values = [ [ 'yoda', 'test post' ] ]
|
210
|
-
# BlogPost.import posts, :
|
210
|
+
# BlogPost.import posts, synchronize: [ post ]
|
211
211
|
# puts post.author_name # => 'yoda'
|
212
212
|
#
|
213
213
|
# # Example synchronizing unsaved/new instances in memory by using a uniqued imported field
|
214
|
-
# posts = [BlogPost.new(:
|
215
|
-
# BlogPost.import posts, :
|
214
|
+
# posts = [BlogPost.new(title: "Foo"), BlogPost.new(title: "Bar")]
|
215
|
+
# BlogPost.import posts, synchronize: posts, synchronize_keys: [:title]
|
216
216
|
# puts posts.first.persisted? # => true
|
217
217
|
#
|
218
218
|
# == On Duplicate Key Update (MySQL)
|
@@ -225,7 +225,7 @@ class ActiveRecord::Base
|
|
225
225
|
# names. The column names are the only fields that are updated if
|
226
226
|
# a duplicate record is found. Below is an example:
|
227
227
|
#
|
228
|
-
# BlogPost.import columns, values, :
|
228
|
+
# BlogPost.import columns, values, on_duplicate_key_update: [ :date_modified, :content, :author ]
|
229
229
|
#
|
230
230
|
# ==== Using A Hash
|
231
231
|
#
|
@@ -234,7 +234,7 @@ class ActiveRecord::Base
|
|
234
234
|
# control over what fields are updated with what attributes on your
|
235
235
|
# model. Below is an example:
|
236
236
|
#
|
237
|
-
# BlogPost.import columns, attributes, :
|
237
|
+
# BlogPost.import columns, attributes, on_duplicate_key_update: { title: :title }
|
238
238
|
#
|
239
239
|
# == On Duplicate Key Update (Postgres 9.5+)
|
240
240
|
#
|
@@ -249,7 +249,7 @@ class ActiveRecord::Base
|
|
249
249
|
# not work. The column names are the only fields that are updated
|
250
250
|
# if a duplicate record is found. Below is an example:
|
251
251
|
#
|
252
|
-
# BlogPost.import columns, values, :
|
252
|
+
# BlogPost.import columns, values, on_duplicate_key_update: [ :date_modified, :content, :author ]
|
253
253
|
#
|
254
254
|
# ==== Using a Hash
|
255
255
|
#
|
@@ -266,7 +266,7 @@ class ActiveRecord::Base
|
|
266
266
|
# but it is the preferred method of identifying a constraint. It will
|
267
267
|
# default to the primary key. Below is an example:
|
268
268
|
#
|
269
|
-
# BlogPost.import columns, values, :
|
269
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: [:author_id, :slug], columns: [ :date_modified ] }
|
270
270
|
#
|
271
271
|
# ====== :constraint_name
|
272
272
|
#
|
@@ -274,7 +274,7 @@ class ActiveRecord::Base
|
|
274
274
|
# unique index by name. Postgres documentation discourages using this method
|
275
275
|
# of identifying an index unless absolutely necessary. Below is an example:
|
276
276
|
#
|
277
|
-
# BlogPost.import columns, values, :
|
277
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { constraint_name: :blog_posts_pkey, columns: [ :date_modified ] }
|
278
278
|
#
|
279
279
|
# ====== :columns
|
280
280
|
#
|
@@ -286,7 +286,7 @@ class ActiveRecord::Base
|
|
286
286
|
# are the only fields that are updated if a duplicate record is found.
|
287
287
|
# Below is an example:
|
288
288
|
#
|
289
|
-
# BlogPost.import columns, values, :
|
289
|
+
# BlogPost.import columns, values, on_duplicate_key_update: { conflict_target: :slug, columns: [ :date_modified, :content, :author ] }
|
290
290
|
#
|
291
291
|
# ======== Using a Hash
|
292
292
|
#
|
@@ -294,7 +294,7 @@ class ActiveRecord::Base
|
|
294
294
|
# mappings. This gives you finer grained control over what fields are updated
|
295
295
|
# with what attributes on your model. Below is an example:
|
296
296
|
#
|
297
|
-
# BlogPost.import columns, attributes, :
|
297
|
+
# BlogPost.import columns, attributes, on_duplicate_key_update: { conflict_target: :slug, columns: { title: :title } }
|
298
298
|
#
|
299
299
|
# = Returns
|
300
300
|
# This returns an object which responds to +failed_instances+ and +num_inserts+.
|
@@ -302,7 +302,7 @@ class ActiveRecord::Base
|
|
302
302
|
# * num_inserts - the number of insert statements it took to import the data
|
303
303
|
# * ids - the primary keys of the imported ids, if the adpater supports it, otherwise and empty array.
|
304
304
|
def import(*args)
|
305
|
-
if args.first.is_a?( Array )
|
305
|
+
if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
|
306
306
|
options = {}
|
307
307
|
options.merge!( args.pop ) if args.last.is_a?(Hash)
|
308
308
|
|
@@ -313,8 +313,19 @@ class ActiveRecord::Base
|
|
313
313
|
end
|
314
314
|
end
|
315
315
|
|
316
|
+
# Imports a collection of values if all values are valid. Import fails at the
|
317
|
+
# first encountered validation error and raises ActiveRecord::RecordInvalid
|
318
|
+
# with the failed instance.
|
319
|
+
def import!(*args)
|
320
|
+
options = args.last.is_a?( Hash ) ? args.pop : {}
|
321
|
+
options[:validate] = true
|
322
|
+
options[:raise_error] = true
|
323
|
+
|
324
|
+
import(*args, options)
|
325
|
+
end
|
326
|
+
|
316
327
|
def import_helper( *args )
|
317
|
-
options = { :
|
328
|
+
options = { validate: true, timestamps: true, primary_key: primary_key }
|
318
329
|
options.merge!( args.pop ) if args.last.is_a? Hash
|
319
330
|
|
320
331
|
# Don't modify incoming arguments
|
@@ -326,7 +337,7 @@ class ActiveRecord::Base
|
|
326
337
|
is_validating = true unless options[:validate_with_context].nil?
|
327
338
|
|
328
339
|
# assume array of model objects
|
329
|
-
if args.last.is_a?( Array )
|
340
|
+
if args.last.is_a?( Array ) && args.last.first.is_a?(ActiveRecord::Base)
|
330
341
|
if args.length == 2
|
331
342
|
models = args.last
|
332
343
|
column_names = args.first
|
@@ -338,26 +349,24 @@ class ActiveRecord::Base
|
|
338
349
|
array_of_attributes = models.map do |model|
|
339
350
|
# this next line breaks sqlite.so with a segmentation fault
|
340
351
|
# if model.new_record? || options[:on_duplicate_key_update]
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
else
|
348
|
-
model.read_attribute_before_type_cast(name)
|
349
|
-
end
|
352
|
+
column_names.map do |name|
|
353
|
+
name = name.to_s
|
354
|
+
if respond_to?(:defined_enums) && defined_enums.key?(name) # ActiveRecord 5
|
355
|
+
model.read_attribute(name)
|
356
|
+
else
|
357
|
+
model.read_attribute_before_type_cast(name)
|
350
358
|
end
|
359
|
+
end
|
351
360
|
# end
|
352
361
|
end
|
353
362
|
# supports empty array
|
354
|
-
elsif args.last.is_a?( Array )
|
363
|
+
elsif args.last.is_a?( Array ) && args.last.empty?
|
355
364
|
return ActiveRecord::Import::Result.new([], 0, []) if args.last.empty?
|
356
365
|
# supports 2-element array and array
|
357
|
-
elsif args.size == 2
|
366
|
+
elsif args.size == 2 && args.first.is_a?( Array ) && args.last.is_a?( Array )
|
358
367
|
column_names, array_of_attributes = args
|
359
368
|
else
|
360
|
-
raise ArgumentError
|
369
|
+
raise ArgumentError, "Invalid arguments!"
|
361
370
|
end
|
362
371
|
|
363
372
|
# dup the passed in array so we don't modify it unintentionally
|
@@ -368,13 +377,13 @@ class ActiveRecord::Base
|
|
368
377
|
# on the list and we are using a sequence and stuff a nil
|
369
378
|
# value for it into each row so the sequencer will fire later
|
370
379
|
if !column_names.include?(primary_key) && connection.prefetch_primary_key? && sequence_name
|
371
|
-
|
372
|
-
|
380
|
+
column_names << primary_key
|
381
|
+
array_of_attributes.each { |a| a << nil }
|
373
382
|
end
|
374
383
|
|
375
384
|
# record timestamps unless disabled in ActiveRecord::Base
|
376
385
|
if record_timestamps && options.delete( :timestamps )
|
377
|
-
|
386
|
+
add_special_rails_stamps column_names, array_of_attributes, options
|
378
387
|
end
|
379
388
|
|
380
389
|
return_obj = if is_validating
|
@@ -385,7 +394,7 @@ class ActiveRecord::Base
|
|
385
394
|
end
|
386
395
|
|
387
396
|
if options[:synchronize]
|
388
|
-
sync_keys = options[:synchronize_keys] || [
|
397
|
+
sync_keys = options[:synchronize_keys] || [primary_key]
|
389
398
|
synchronize( options[:synchronize], sync_keys)
|
390
399
|
end
|
391
400
|
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
@@ -395,9 +404,7 @@ class ActiveRecord::Base
|
|
395
404
|
set_ids_and_mark_clean(models, return_obj)
|
396
405
|
|
397
406
|
# if there are auto-save associations on the models we imported that are new, import them as well
|
398
|
-
if options[:recursive]
|
399
|
-
import_associations(models, options)
|
400
|
-
end
|
407
|
+
import_associations(models, options.dup) if options[:recursive]
|
401
408
|
end
|
402
409
|
|
403
410
|
return_obj
|
@@ -414,7 +421,7 @@ class ActiveRecord::Base
|
|
414
421
|
# +num_inserts+ is the number of inserts it took to import the data. See
|
415
422
|
# ActiveRecord::Base.import for more information on
|
416
423
|
# +column_names+, +array_of_attributes+ and +options+.
|
417
|
-
def import_with_validations( column_names, array_of_attributes, options={} )
|
424
|
+
def import_with_validations( column_names, array_of_attributes, options = {} )
|
418
425
|
failed_instances = []
|
419
426
|
|
420
427
|
# create instances for each of our column/value sets
|
@@ -422,23 +429,23 @@ class ActiveRecord::Base
|
|
422
429
|
|
423
430
|
# keep track of the instance and the position it is currently at. if this fails
|
424
431
|
# validation we'll use the index to remove it from the array_of_attributes
|
425
|
-
arr.each_with_index do |hsh,i|
|
432
|
+
arr.each_with_index do |hsh, i|
|
426
433
|
instance = new do |model|
|
427
|
-
hsh.each_pair{ |k,v| model
|
434
|
+
hsh.each_pair { |k, v| model[k] = v }
|
428
435
|
end
|
429
436
|
|
430
|
-
if
|
431
|
-
|
432
|
-
|
433
|
-
|
437
|
+
next if instance.valid?(options[:validate_with_context])
|
438
|
+
raise(ActiveRecord::RecordInvalid, instance) if options[:raise_error]
|
439
|
+
array_of_attributes[i] = nil
|
440
|
+
failed_instances << instance
|
434
441
|
end
|
435
442
|
array_of_attributes.compact!
|
436
443
|
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
444
|
+
num_inserts, ids = if array_of_attributes.empty? || options[:all_or_none] && failed_instances.any?
|
445
|
+
[0, []]
|
446
|
+
else
|
447
|
+
import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
448
|
+
end
|
442
449
|
ActiveRecord::Import::Result.new(failed_instances, num_inserts, ids)
|
443
450
|
end
|
444
451
|
|
@@ -448,7 +455,7 @@ class ActiveRecord::Base
|
|
448
455
|
# validations or callbacks. See ActiveRecord::Base.import for more
|
449
456
|
# information on +column_names+, +array_of_attributes_ and
|
450
457
|
# +options+.
|
451
|
-
def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
|
458
|
+
def import_without_validations_or_callbacks( column_names, array_of_attributes, options = {} )
|
452
459
|
column_names = column_names.map(&:to_sym)
|
453
460
|
scope_columns, scope_values = scope_attributes.to_a.transpose
|
454
461
|
|
@@ -473,24 +480,30 @@ class ActiveRecord::Base
|
|
473
480
|
column
|
474
481
|
end
|
475
482
|
|
476
|
-
columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
|
477
|
-
insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
|
483
|
+
columns_sql = "(#{column_names.map { |name| connection.quote_column_name(name) }.join(',')})"
|
484
|
+
insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ' : ''}INTO #{quoted_table_name} #{columns_sql} VALUES "
|
478
485
|
values_sql = values_sql_for_columns_and_attributes(columns, array_of_attributes)
|
486
|
+
|
487
|
+
number_inserted = 0
|
479
488
|
ids = []
|
480
|
-
if
|
481
|
-
|
489
|
+
if supports_import?
|
490
|
+
# generate the sql
|
491
|
+
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
492
|
+
|
493
|
+
batch_size = options[:batch_size] || values_sql.size
|
494
|
+
values_sql.each_slice(batch_size) do |batch_values|
|
495
|
+
# perform the inserts
|
496
|
+
result = connection.insert_many( [insert_sql, post_sql_statements].flatten,
|
497
|
+
batch_values,
|
498
|
+
"#{self.class.name} Create Many Without Validations Or Callbacks" )
|
499
|
+
number_inserted += result[0]
|
500
|
+
ids += result[1]
|
501
|
+
end
|
502
|
+
else
|
482
503
|
values_sql.each do |values|
|
483
504
|
connection.execute(insert_sql + values)
|
484
505
|
number_inserted += 1
|
485
506
|
end
|
486
|
-
else
|
487
|
-
# generate the sql
|
488
|
-
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
489
|
-
|
490
|
-
# perform the inserts
|
491
|
-
(number_inserted,ids) = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
|
492
|
-
values_sql,
|
493
|
-
"#{self.class.name} Create Many Without Validations Or Callbacks" )
|
494
507
|
end
|
495
508
|
[number_inserted, ids]
|
496
509
|
end
|
@@ -498,17 +511,16 @@ class ActiveRecord::Base
|
|
498
511
|
private
|
499
512
|
|
500
513
|
def set_ids_and_mark_clean(models, import_result)
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
end
|
510
|
-
model.instance_variable_set(:@new_record, false)
|
514
|
+
return if models.nil?
|
515
|
+
import_result.ids.each_with_index do |id, index|
|
516
|
+
model = models[index]
|
517
|
+
model.id = id.to_i
|
518
|
+
if model.respond_to?(:clear_changes_information) # Rails 4.0 and higher
|
519
|
+
model.clear_changes_information
|
520
|
+
else # Rails 3.1
|
521
|
+
model.instance_variable_get(:@changed_attributes).clear
|
511
522
|
end
|
523
|
+
model.instance_variable_set(:@new_record, false)
|
512
524
|
end
|
513
525
|
end
|
514
526
|
|
@@ -517,11 +529,14 @@ class ActiveRecord::Base
|
|
517
529
|
# notes:
|
518
530
|
# does not handle associations that reference themselves
|
519
531
|
# should probably take a hash to associations to follow.
|
520
|
-
associated_objects_by_class={}
|
521
|
-
models.each {|model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
532
|
+
associated_objects_by_class = {}
|
533
|
+
models.each { |model| find_associated_objects_for_import(associated_objects_by_class, model) }
|
534
|
+
|
535
|
+
# :on_duplicate_key_update not supported for associations
|
536
|
+
options.delete(:on_duplicate_key_update)
|
522
537
|
|
523
|
-
associated_objects_by_class.
|
524
|
-
associations.
|
538
|
+
associated_objects_by_class.each_value do |associations|
|
539
|
+
associations.each_value do |associated_records|
|
525
540
|
associated_records.first.class.import(associated_records, options) unless associated_records.empty?
|
526
541
|
end
|
527
542
|
end
|
@@ -530,13 +545,13 @@ class ActiveRecord::Base
|
|
530
545
|
# We are eventually going to call Class.import <objects> so we build up a hash
|
531
546
|
# of class => objects to import.
|
532
547
|
def find_associated_objects_for_import(associated_objects_by_class, model)
|
533
|
-
associated_objects_by_class[model.class.name]||={}
|
548
|
+
associated_objects_by_class[model.class.name] ||= {}
|
534
549
|
|
535
550
|
association_reflections =
|
536
551
|
model.class.reflect_on_all_associations(:has_one) +
|
537
552
|
model.class.reflect_on_all_associations(:has_many)
|
538
553
|
association_reflections.each do |association_reflection|
|
539
|
-
associated_objects_by_class[model.class.name][association_reflection.name]||=[]
|
554
|
+
associated_objects_by_class[model.class.name][association_reflection.name] ||= []
|
540
555
|
|
541
556
|
association = model.association(association_reflection.name)
|
542
557
|
association.loaded!
|
@@ -544,9 +559,13 @@ class ActiveRecord::Base
|
|
544
559
|
# Wrap target in an array if not already
|
545
560
|
association = Array(association.target)
|
546
561
|
|
547
|
-
changed_objects = association.select {|a| a.new_record? || a.changed?}
|
562
|
+
changed_objects = association.select { |a| a.new_record? || a.changed? }
|
548
563
|
changed_objects.each do |child|
|
549
|
-
child.
|
564
|
+
child.public_send("#{association_reflection.foreign_key}=", model.id)
|
565
|
+
# For polymorphic associations
|
566
|
+
association_reflection.type.try do |type|
|
567
|
+
child.public_send("#{type}=", model.class.name)
|
568
|
+
end
|
550
569
|
end
|
551
570
|
associated_objects_by_class[model.class.name][association_reflection.name].concat changed_objects
|
552
571
|
end
|
@@ -555,23 +574,26 @@ class ActiveRecord::Base
|
|
555
574
|
|
556
575
|
# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
|
557
576
|
# and +array_of_attributes+.
|
558
|
-
def values_sql_for_columns_and_attributes(columns, array_of_attributes)
|
577
|
+
def values_sql_for_columns_and_attributes(columns, array_of_attributes) # :nodoc:
|
559
578
|
# connection gets called a *lot* in this high intensity loop.
|
560
579
|
# Reuse the same one w/in the loop, otherwise it would keep being re-retreived (= lots of time for large imports)
|
561
580
|
connection_memo = connection
|
562
581
|
array_of_attributes.map do |arr|
|
563
|
-
my_values = arr.each_with_index.map do |val,j|
|
582
|
+
my_values = arr.each_with_index.map do |val, j|
|
564
583
|
column = columns[j]
|
565
584
|
|
566
585
|
# be sure to query sequence_name *last*, only if cheaper tests fail, because it's costly
|
567
586
|
if val.nil? && column.name == primary_key && !sequence_name.blank?
|
568
|
-
|
587
|
+
connection_memo.next_value_for_sequence(sequence_name)
|
569
588
|
elsif column
|
570
589
|
if respond_to?(:type_caster) && type_caster.respond_to?(:type_cast_for_database) # Rails 5.0 and higher
|
571
590
|
connection_memo.quote(type_caster.type_cast_for_database(column.name, val))
|
572
|
-
elsif column.respond_to?(:type_cast_from_user)
|
591
|
+
elsif column.respond_to?(:type_cast_from_user) # Rails 4.2 and higher
|
573
592
|
connection_memo.quote(column.type_cast_from_user(val), column)
|
574
|
-
else
|
593
|
+
else # Rails 3.1, 3.2, 4.0 and 4.1
|
594
|
+
if serialized_attributes.include?(column.name)
|
595
|
+
val = serialized_attributes[column.name].dump(val)
|
596
|
+
end
|
575
597
|
connection_memo.quote(column.type_cast(val), column)
|
576
598
|
end
|
577
599
|
end
|
@@ -582,32 +604,32 @@ class ActiveRecord::Base
|
|
582
604
|
|
583
605
|
def add_special_rails_stamps( column_names, array_of_attributes, options )
|
584
606
|
AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
607
|
+
next unless self.column_names.include?(key)
|
608
|
+
value = blk.call
|
609
|
+
index = column_names.index(key) || column_names.index(key.to_sym)
|
610
|
+
if index
|
611
|
+
# replace every instance of the array of attributes with our value
|
612
|
+
array_of_attributes.each { |arr| arr[index] = value if arr[index].nil? }
|
613
|
+
else
|
614
|
+
column_names << key
|
615
|
+
array_of_attributes.each { |arr| arr << value }
|
594
616
|
end
|
595
617
|
end
|
596
618
|
|
597
619
|
AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
620
|
+
next unless self.column_names.include?(key)
|
621
|
+
value = blk.call
|
622
|
+
index = column_names.index(key) || column_names.index(key.to_sym)
|
623
|
+
if index
|
624
|
+
# replace every instance of the array of attributes with our value
|
625
|
+
array_of_attributes.each { |arr| arr[index] = value }
|
626
|
+
else
|
627
|
+
column_names << key
|
628
|
+
array_of_attributes.each { |arr| arr << value }
|
629
|
+
end
|
607
630
|
|
608
|
-
|
609
|
-
|
610
|
-
end
|
631
|
+
if supports_on_duplicate_key_update?
|
632
|
+
connection.add_column_for_on_duplicate_key_update(key, options)
|
611
633
|
end
|
612
634
|
end
|
613
635
|
end
|
@@ -615,9 +637,8 @@ class ActiveRecord::Base
|
|
615
637
|
# Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
|
616
638
|
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
617
639
|
array_of_attributes.map do |attributes|
|
618
|
-
Hash[attributes.each_with_index.map {|attr, c| [column_names[c], attr] }]
|
640
|
+
Hash[attributes.each_with_index.map { |attr, c| [column_names[c], attr] }]
|
619
641
|
end
|
620
642
|
end
|
621
|
-
|
622
643
|
end
|
623
644
|
end
|