Empact-activerecord-import 0.3.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.
- data/README.markdown +25 -0
- data/Rakefile +72 -0
- data/VERSION +1 -0
- data/lib/activerecord-import.rb +16 -0
- data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +10 -0
- data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
- data/lib/activerecord-import/active_record/adapters/mysql_adapter.rb +6 -0
- data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +7 -0
- data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +7 -0
- data/lib/activerecord-import/adapters/abstract_adapter.rb +125 -0
- data/lib/activerecord-import/adapters/mysql_adapter.rb +50 -0
- data/lib/activerecord-import/adapters/postgresql_adapter.rb +13 -0
- data/lib/activerecord-import/adapters/sqlite3_adapter.rb +7 -0
- data/lib/activerecord-import/base.rb +27 -0
- data/lib/activerecord-import/import.rb +347 -0
- data/lib/activerecord-import/mysql.rb +8 -0
- data/lib/activerecord-import/mysql2.rb +8 -0
- data/lib/activerecord-import/postgresql.rb +8 -0
- data/lib/activerecord-import/sqlite3.rb +8 -0
- data/test/active_record/connection_adapter_test.rb +52 -0
- data/test/adapters/mysql.rb +1 -0
- data/test/adapters/mysql2.rb +1 -0
- data/test/adapters/postgresql.rb +1 -0
- data/test/adapters/sqlite3.rb +1 -0
- data/test/import_test.rb +202 -0
- data/test/models/book.rb +3 -0
- data/test/models/group.rb +3 -0
- data/test/models/topic.rb +7 -0
- data/test/mysql/import_test.rb +6 -0
- data/test/mysql2/import_test.rb +6 -0
- data/test/postgresql/import_test.rb +20 -0
- data/test/schema/generic_schema.rb +98 -0
- data/test/schema/mysql_schema.rb +17 -0
- data/test/schema/version.rb +4 -0
- data/test/support/active_support/test_case_extensions.rb +67 -0
- data/test/support/factories.rb +13 -0
- data/test/support/generate.rb +29 -0
- data/test/support/mysql/assertions.rb +55 -0
- data/test/support/mysql/import_examples.rb +117 -0
- data/test/test_helper.rb +46 -0
- metadata +169 -0
data/README.markdown
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# activerecord-import
|
2
|
+
|
3
|
+
activerecord-import is a library for bulk inserting data using ActiveRecord.
|
4
|
+
|
5
|
+
For more information on activerecord-import please see its wiki: http://wiki.github.com/zdennis/activerecord-import/
|
6
|
+
|
7
|
+
# License
|
8
|
+
|
9
|
+
This is licensed under the ruby license.
|
10
|
+
|
11
|
+
# Author
|
12
|
+
|
13
|
+
Zach Dennis (zach.dennis@gmail.com)
|
14
|
+
|
15
|
+
# Contributors
|
16
|
+
|
17
|
+
* Blythe Dunham
|
18
|
+
* Gabe da Silveira
|
19
|
+
* Henry Work
|
20
|
+
* James Herdman
|
21
|
+
* Marcus Crafter
|
22
|
+
* Thibaud Guillaume-Gentil
|
23
|
+
* Mark Van Holstyn
|
24
|
+
* Victor Costan
|
25
|
+
* Ben Woosley - Postgres support, general cleanup
|
data/Rakefile
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
require "bundler"
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
begin
|
8
|
+
require 'jeweler'
|
9
|
+
Jeweler::Tasks.new do |gem|
|
10
|
+
gem.name = "Empact-activerecord-import"
|
11
|
+
gem.summary = %Q{Bulk-loading extension for ActiveRecord}
|
12
|
+
gem.description = %Q{Extraction of the ActiveRecord::Base#import functionality from ar-extensions for Rails 3 and beyond}
|
13
|
+
gem.email = "ben.woosley@gmail.com"
|
14
|
+
gem.homepage = "http://github.com/Empact/activerecord-import"
|
15
|
+
gem.authors = ["Zach Dennis", "Ben Woosley"]
|
16
|
+
gem.files = FileList["VERSION", "Rakefile", "README*", "lib/**/*"]
|
17
|
+
|
18
|
+
bundler = Bundler.load
|
19
|
+
bundler.dependencies_for(:default).each do |dependency|
|
20
|
+
gem.add_dependency dependency.name, *dependency.requirements_list
|
21
|
+
end
|
22
|
+
|
23
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
24
|
+
end
|
25
|
+
Jeweler::GemcutterTasks.new
|
26
|
+
rescue LoadError
|
27
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
28
|
+
end
|
29
|
+
|
30
|
+
namespace :display do
|
31
|
+
task :notice do
|
32
|
+
puts
|
33
|
+
puts "To run tests you must supply the adapter, see rake -T for more information."
|
34
|
+
puts
|
35
|
+
end
|
36
|
+
end
|
37
|
+
task :default => ["display:notice"]
|
38
|
+
|
39
|
+
ADAPTERS = %w(mysql mysql2 postgresql sqlite3)
|
40
|
+
ADAPTERS.each do |adapter|
|
41
|
+
namespace :test do
|
42
|
+
desc "Runs #{adapter} database tests."
|
43
|
+
Rake::TestTask.new(adapter) do |t|
|
44
|
+
t.test_files = FileList["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/#{adapter}/**/*_test.rb"]
|
45
|
+
end
|
46
|
+
task adapter
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
require 'rcov/rcovtask'
|
52
|
+
adapter = ENV['ARE_DB']
|
53
|
+
Rcov::RcovTask.new do |test|
|
54
|
+
test.libs << 'test'
|
55
|
+
test.pattern = ["test/adapters/#{adapter}.rb", "test/*_test.rb", "test/#{adapter}/**/*_test.rb"]
|
56
|
+
test.verbose = true
|
57
|
+
end
|
58
|
+
rescue LoadError
|
59
|
+
task :rcov do
|
60
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
require 'rake/rdoctask'
|
65
|
+
Rake::RDocTask.new do |rdoc|
|
66
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
67
|
+
|
68
|
+
rdoc.rdoc_dir = 'rdoc'
|
69
|
+
rdoc.title = "activerecord-import #{version}"
|
70
|
+
rdoc.rdoc_files.include('README*')
|
71
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
72
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.0
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
def establish_connection_with_activerecord_import(*args)
|
4
|
+
establish_connection_without_activerecord_import(*args)
|
5
|
+
ActiveSupport.run_load_hooks(:active_record_connection_established, connection)
|
6
|
+
end
|
7
|
+
alias_method_chain :establish_connection, :activerecord_import
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
ActiveSupport.on_load(:active_record_connection_established) do |connection|
|
12
|
+
if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection)
|
13
|
+
require File.join File.dirname(__FILE__), "activerecord-import/base"
|
14
|
+
end
|
15
|
+
ActiveRecord::Import.load_from_connection connection
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "activerecord-import/adapters/abstract_adapter"
|
2
|
+
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
module ConnectionAdapters # :nodoc:
|
5
|
+
class AbstractAdapter # :nodoc:
|
6
|
+
extend ActiveRecord::Import::AbstractAdapter::ClassMethods
|
7
|
+
include ActiveRecord::Import::AbstractAdapter::InstanceMethods
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module ActiveRecord::Import::AbstractAdapter
|
2
|
+
NO_MAX_PACKET = 0
|
3
|
+
QUERY_OVERHEAD = 8 #This was shown to be true for MySQL, but it's not clear where the overhead is from.
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
# Returns the sum of the sizes of the passed in objects. This should
|
7
|
+
# probably be moved outside this class, but to where?
|
8
|
+
def sum_sizes( *objects ) # :nodoc:
|
9
|
+
objects.inject( 0 ){|sum,o| sum += o.size }
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc:
|
13
|
+
value_sets = []
|
14
|
+
arr, current_arr_values_size, current_size = [], 0, 0
|
15
|
+
values.each_with_index do |val,i|
|
16
|
+
comma_bytes = arr.size
|
17
|
+
sql_size_thus_far = sql_size + current_size + val.size + comma_bytes
|
18
|
+
if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes
|
19
|
+
current_size += val.size
|
20
|
+
arr << val
|
21
|
+
else
|
22
|
+
value_sets << arr
|
23
|
+
arr = [ val ]
|
24
|
+
current_size = val.size
|
25
|
+
end
|
26
|
+
|
27
|
+
# if we're on the last iteration push whatever we have in arr to value_sets
|
28
|
+
value_sets << arr if i == (values.size-1)
|
29
|
+
end
|
30
|
+
[ *value_sets ]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
module InstanceMethods
|
35
|
+
def next_value_for_sequence(sequence_name)
|
36
|
+
%{#{sequence_name}.nextval}
|
37
|
+
end
|
38
|
+
|
39
|
+
# +sql+ can be a single string or an array. If it is an array all
|
40
|
+
# elements that are in position >= 1 will be appended to the final SQL.
|
41
|
+
def insert_many( sql, values, *args ) # :nodoc:
|
42
|
+
# the number of inserts default
|
43
|
+
number_of_inserts = 0
|
44
|
+
|
45
|
+
base_sql,post_sql = if sql.is_a?( String )
|
46
|
+
[ sql, '' ]
|
47
|
+
elsif sql.is_a?( Array )
|
48
|
+
[ sql.shift, sql.join( ' ' ) ]
|
49
|
+
end
|
50
|
+
|
51
|
+
sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
|
52
|
+
|
53
|
+
# the number of bytes the requested insert statement values will take up
|
54
|
+
values_in_bytes = self.class.sum_sizes( *values )
|
55
|
+
|
56
|
+
# the number of bytes (commas) it will take to comma separate our values
|
57
|
+
comma_separated_bytes = values.size-1
|
58
|
+
|
59
|
+
# the total number of bytes required if this statement is one statement
|
60
|
+
total_bytes = sql_size + values_in_bytes + comma_separated_bytes
|
61
|
+
|
62
|
+
max = max_allowed_packet
|
63
|
+
|
64
|
+
# if we can insert it all as one statement
|
65
|
+
if NO_MAX_PACKET == max or total_bytes < max
|
66
|
+
number_of_inserts += 1
|
67
|
+
sql2insert = base_sql + values.join( ',' ) + post_sql
|
68
|
+
insert( sql2insert, *args )
|
69
|
+
else
|
70
|
+
value_sets = self.class.get_insert_value_sets( values, sql_size, max )
|
71
|
+
value_sets.each do |values|
|
72
|
+
number_of_inserts += 1
|
73
|
+
sql2insert = base_sql + values.join( ',' ) + post_sql
|
74
|
+
insert( sql2insert, *args )
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
number_of_inserts
|
79
|
+
end
|
80
|
+
|
81
|
+
def pre_sql_statements(options)
|
82
|
+
sql = []
|
83
|
+
sql << options[:pre_sql] if options[:pre_sql]
|
84
|
+
sql << options[:command] if options[:command]
|
85
|
+
sql << "IGNORE" if options[:ignore]
|
86
|
+
|
87
|
+
#add keywords like IGNORE or DELAYED
|
88
|
+
if options[:keywords].is_a?(Array)
|
89
|
+
sql.concat(options[:keywords])
|
90
|
+
elsif options[:keywords]
|
91
|
+
sql << options[:keywords].to_s
|
92
|
+
end
|
93
|
+
|
94
|
+
sql
|
95
|
+
end
|
96
|
+
|
97
|
+
# Synchronizes the passed in ActiveRecord instances with the records in
|
98
|
+
# the database by calling +reload+ on each instance.
|
99
|
+
def after_import_synchronize( instances )
|
100
|
+
instances.each { |e| e.reload }
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns an array of post SQL statements given the passed in options.
|
104
|
+
def post_sql_statements( table_name, options ) # :nodoc:
|
105
|
+
post_sql_statements = []
|
106
|
+
if options[:on_duplicate_key_update]
|
107
|
+
post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
|
108
|
+
end
|
109
|
+
|
110
|
+
#custom user post_sql
|
111
|
+
post_sql_statements << options[:post_sql] if options[:post_sql]
|
112
|
+
|
113
|
+
#with rollup
|
114
|
+
post_sql_statements << rollup_sql if options[:rollup]
|
115
|
+
|
116
|
+
post_sql_statements
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns the maximum number of bytes that the server will allow
|
120
|
+
# in a single packet
|
121
|
+
def max_allowed_packet
|
122
|
+
NO_MAX_PACKET
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module ActiveRecord::Import::MysqlAdapter
|
2
|
+
module InstanceMethods
|
3
|
+
def self.included(klass)
|
4
|
+
klass.instance_eval do
|
5
|
+
include ActiveRecord::Import::ImportSupport
|
6
|
+
include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
|
11
|
+
# in +args+.
|
12
|
+
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
13
|
+
sql = ' ON DUPLICATE KEY UPDATE '
|
14
|
+
arg = args.first
|
15
|
+
if arg.is_a?( Array )
|
16
|
+
sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
|
17
|
+
elsif arg.is_a?( Hash )
|
18
|
+
sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
|
19
|
+
elsif arg.is_a?( String )
|
20
|
+
sql << arg
|
21
|
+
else
|
22
|
+
raise ArgumentError.new( "Expected Array or Hash" )
|
23
|
+
end
|
24
|
+
sql
|
25
|
+
end
|
26
|
+
|
27
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
|
28
|
+
results = arr.map do |column|
|
29
|
+
qc = quote_column_name( column )
|
30
|
+
"#{table_name}.#{qc}=VALUES(#{qc})"
|
31
|
+
end
|
32
|
+
results.join( ',' )
|
33
|
+
end
|
34
|
+
|
35
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
36
|
+
sql = ' ON DUPLICATE KEY UPDATE '
|
37
|
+
results = hsh.map do |column1, column2|
|
38
|
+
qc1 = quote_column_name( column1 )
|
39
|
+
qc2 = quote_column_name( column2 )
|
40
|
+
"#{table_name}.#{qc1}=VALUES( #{qc2} )"
|
41
|
+
end
|
42
|
+
results.join( ',')
|
43
|
+
end
|
44
|
+
|
45
|
+
#return true if the statement is a duplicate key record error
|
46
|
+
def duplicate_key_update_error?(exception)# :nodoc:
|
47
|
+
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveRecord::Import::PostgreSQLAdapter
|
2
|
+
module InstanceMethods
|
3
|
+
def self.included(klass)
|
4
|
+
klass.instance_eval do
|
5
|
+
include ActiveRecord::Import::ImportSupport
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def next_value_for_sequence(sequence_name)
|
10
|
+
%{nextval('#{sequence_name}')}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "pathname"
|
2
|
+
require "active_record"
|
3
|
+
require "active_record/version"
|
4
|
+
|
5
|
+
module ActiveRecord::Import
|
6
|
+
AdapterPath = File.join File.expand_path(File.dirname(__FILE__)), "/active_record/adapters"
|
7
|
+
|
8
|
+
# Loads the import functionality for a specific database adapter
|
9
|
+
def self.require_adapter(adapter)
|
10
|
+
require File.join(AdapterPath,"/abstract_adapter")
|
11
|
+
require File.join(AdapterPath,"/#{adapter}_adapter")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Loads the import functionality for the passed in ActiveRecord connection
|
15
|
+
def self.load_from_connection(connection)
|
16
|
+
import_adapter = "ActiveRecord::Import::#{connection.class.name.demodulize}::InstanceMethods"
|
17
|
+
unless connection.class.ancestors.map(&:name).include?(import_adapter)
|
18
|
+
config = connection.instance_variable_get :@config
|
19
|
+
require_adapter config[:adapter]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
this_dir = Pathname.new File.dirname(__FILE__)
|
26
|
+
require this_dir.join("import")
|
27
|
+
require this_dir.join("active_record/adapters/abstract_adapter")
|
@@ -0,0 +1,347 @@
|
|
1
|
+
require "ostruct"
|
2
|
+
|
3
|
+
module ActiveRecord::Import::ConnectionAdapters ; end
|
4
|
+
|
5
|
+
module ActiveRecord::Import #:nodoc:
|
6
|
+
class Result < Struct.new(:failed_instances, :num_inserts)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ImportSupport #:nodoc:
|
10
|
+
def supports_import? #:nodoc:
|
11
|
+
true
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module OnDuplicateKeyUpdateSupport #:nodoc:
|
16
|
+
def supports_on_duplicate_key_update? #:nodoc:
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ActiveRecord::Base
|
23
|
+
class << self
|
24
|
+
|
25
|
+
# use tz as set in ActiveRecord::Base
|
26
|
+
tproc = lambda do
|
27
|
+
ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
|
28
|
+
end
|
29
|
+
|
30
|
+
AREXT_RAILS_COLUMNS = {
|
31
|
+
:create => { "created_on" => tproc ,
|
32
|
+
"created_at" => tproc },
|
33
|
+
:update => { "updated_on" => tproc ,
|
34
|
+
"updated_at" => tproc }
|
35
|
+
}
|
36
|
+
AREXT_RAILS_COLUMN_NAMES = AREXT_RAILS_COLUMNS[:create].keys + AREXT_RAILS_COLUMNS[:update].keys
|
37
|
+
|
38
|
+
# Returns true if the current database connection adapter
|
39
|
+
# supports import functionality, otherwise returns false.
|
40
|
+
def supports_import?
|
41
|
+
connection.supports_import?
|
42
|
+
rescue NoMethodError
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns true if the current database connection adapter
|
47
|
+
# supports on duplicate key update functionality, otherwise
|
48
|
+
# returns false.
|
49
|
+
def supports_on_duplicate_key_update?
|
50
|
+
connection.supports_on_duplicate_key_update?
|
51
|
+
rescue NoMethodError
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
# Imports a collection of values to the database.
|
56
|
+
#
|
57
|
+
# This is more efficient than using ActiveRecord::Base#create or
|
58
|
+
# ActiveRecord::Base#save multiple times. This method works well if
|
59
|
+
# you want to create more than one record at a time and do not care
|
60
|
+
# about having ActiveRecord objects returned for each record
|
61
|
+
# inserted.
|
62
|
+
#
|
63
|
+
# This can be used with or without validations. It does not utilize
|
64
|
+
# the ActiveRecord::Callbacks during creation/modification while
|
65
|
+
# performing the import.
|
66
|
+
#
|
67
|
+
# == Usage
|
68
|
+
# Model.import array_of_models
|
69
|
+
# Model.import column_names, array_of_values
|
70
|
+
# Model.import column_names, array_of_values, options
|
71
|
+
#
|
72
|
+
# ==== Model.import array_of_models
|
73
|
+
#
|
74
|
+
# With this form you can call _import_ passing in an array of model
|
75
|
+
# objects that you want updated.
|
76
|
+
#
|
77
|
+
# ==== Model.import column_names, array_of_values
|
78
|
+
#
|
79
|
+
# The first parameter +column_names+ is an array of symbols or
|
80
|
+
# strings which specify the columns that you want to update.
|
81
|
+
#
|
82
|
+
# The second parameter, +array_of_values+, is an array of
|
83
|
+
# arrays. Each subarray is a single set of values for a new
|
84
|
+
# record. The order of values in each subarray should match up to
|
85
|
+
# the order of the +column_names+.
|
86
|
+
#
|
87
|
+
# ==== Model.import column_names, array_of_values, options
|
88
|
+
#
|
89
|
+
# The first two parameters are the same as the above form. The third
|
90
|
+
# parameter, +options+, is a hash. This is optional. Please see
|
91
|
+
# below for what +options+ are available.
|
92
|
+
#
|
93
|
+
# == Options
|
94
|
+
# * +validate+ - true|false, tells import whether or not to use \
|
95
|
+
# ActiveRecord validations. Validations are enforced by default.
|
96
|
+
# * +on_duplicate_key_update+ - an Array or Hash, tells import to \
|
97
|
+
# use MySQL's ON DUPLICATE KEY UPDATE ability. See On Duplicate\
|
98
|
+
# Key Update below.
|
99
|
+
# * +synchronize+ - an array of ActiveRecord instances for the model
|
100
|
+
# that you are currently importing data into. This synchronizes
|
101
|
+
# existing model instances in memory with updates from the import.
|
102
|
+
# * +timestamps+ - true|false, tells import to not add timestamps \
|
103
|
+
# (if false) even if record timestamps is disabled in ActiveRecord::Base
|
104
|
+
#
|
105
|
+
# == Examples
|
106
|
+
# class BlogPost < ActiveRecord::Base ; end
|
107
|
+
#
|
108
|
+
# # Example using array of model objects
|
109
|
+
# posts = [ BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT',
|
110
|
+
# BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT2',
|
111
|
+
# BlogPost.new :author_name=>'Zach Dennis', :title=>'AREXT3' ]
|
112
|
+
# BlogPost.import posts
|
113
|
+
#
|
114
|
+
# # Example using column_names and array_of_values
|
115
|
+
# columns = [ :author_name, :title ]
|
116
|
+
# values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
|
117
|
+
# BlogPost.import columns, values
|
118
|
+
#
|
119
|
+
# # Example using column_names, array_of_value and options
|
120
|
+
# columns = [ :author_name, :title ]
|
121
|
+
# values = [ [ 'zdennis', 'test post' ], [ 'jdoe', 'another test post' ] ]
|
122
|
+
# BlogPost.import( columns, values, :validate => false )
|
123
|
+
#
|
124
|
+
# # Example synchronizing existing instances in memory
|
125
|
+
# post = BlogPost.find_by_author_name( 'zdennis' )
|
126
|
+
# puts post.author_name # => 'zdennis'
|
127
|
+
# columns = [ :author_name, :title ]
|
128
|
+
# values = [ [ 'yoda', 'test post' ] ]
|
129
|
+
# BlogPost.import posts, :synchronize=>[ post ]
|
130
|
+
# puts post.author_name # => 'yoda'
|
131
|
+
#
|
132
|
+
# == On Duplicate Key Update (MySQL only)
|
133
|
+
#
|
134
|
+
# The :on_duplicate_key_update option can be either an Array or a Hash.
|
135
|
+
#
|
136
|
+
# ==== Using an Array
|
137
|
+
#
|
138
|
+
# The :on_duplicate_key_update option can be an array of column
|
139
|
+
# names. The column names are the only fields that are updated if
|
140
|
+
# a duplicate record is found. Below is an example:
|
141
|
+
#
|
142
|
+
# BlogPost.import columns, values, :on_duplicate_key_update=>[ :date_modified, :content, :author ]
|
143
|
+
#
|
144
|
+
# ==== Using A Hash
|
145
|
+
#
|
146
|
+
# The :on_duplicate_key_update option can be a hash of column name
|
147
|
+
# to model attribute name mappings. This gives you finer grained
|
148
|
+
# control over what fields are updated with what attributes on your
|
149
|
+
# model. Below is an example:
|
150
|
+
#
|
151
|
+
# BlogPost.import columns, attributes, :on_duplicate_key_update=>{ :title => :title }
|
152
|
+
#
|
153
|
+
# = Returns
|
154
|
+
# This returns an object which responds to +failed_instances+ and +num_inserts+.
|
155
|
+
# * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
|
156
|
+
# * num_inserts - the number of insert statements it took to import the data
|
157
|
+
def import( *args )
|
158
|
+
options = { :validate=>true, :timestamps=>true }
|
159
|
+
options.merge!( args.pop ) if args.last.is_a? Hash
|
160
|
+
|
161
|
+
is_validating = options.delete( :validate )
|
162
|
+
|
163
|
+
# assume array of model objects
|
164
|
+
if args.last.is_a?( Array ) and args.last.first.is_a? ActiveRecord::Base
|
165
|
+
if args.length == 2
|
166
|
+
models = args.last
|
167
|
+
column_names = args.first
|
168
|
+
else
|
169
|
+
models = args.first
|
170
|
+
column_names = self.column_names.dup
|
171
|
+
end
|
172
|
+
|
173
|
+
array_of_attributes = models.map do |model|
|
174
|
+
# this next line breaks sqlite.so with a segmentation fault
|
175
|
+
# if model.new_record? || options[:on_duplicate_key_update]
|
176
|
+
column_names.map do |name|
|
177
|
+
model.send( "#{name}_before_type_cast" )
|
178
|
+
end
|
179
|
+
# end
|
180
|
+
end
|
181
|
+
# supports 2-element array and array
|
182
|
+
elsif args.size == 2 and args.first.is_a?( Array ) and args.last.is_a?( Array )
|
183
|
+
column_names, array_of_attributes = args
|
184
|
+
else
|
185
|
+
raise ArgumentError.new( "Invalid arguments!" )
|
186
|
+
end
|
187
|
+
|
188
|
+
# dup the passed in array so we don't modify it unintentionally
|
189
|
+
array_of_attributes = array_of_attributes.dup
|
190
|
+
|
191
|
+
# Force the primary key col into the insert if it's not
|
192
|
+
# on the list and we are using a sequence and stuff a nil
|
193
|
+
# value for it into each row so the sequencer will fire later
|
194
|
+
if !column_names.include?(primary_key) && sequence_name && connection.prefetch_primary_key?
|
195
|
+
column_names << primary_key
|
196
|
+
array_of_attributes.each { |a| a << nil }
|
197
|
+
end
|
198
|
+
|
199
|
+
# record timestamps unless disabled in ActiveRecord::Base
|
200
|
+
if record_timestamps && options.delete( :timestamps )
|
201
|
+
add_special_rails_stamps column_names, array_of_attributes, options
|
202
|
+
end
|
203
|
+
|
204
|
+
return_obj = if is_validating
|
205
|
+
import_with_validations( column_names, array_of_attributes, options )
|
206
|
+
else
|
207
|
+
num_inserts = import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
208
|
+
ActiveRecord::Import::Result.new([], num_inserts)
|
209
|
+
end
|
210
|
+
|
211
|
+
if options[:synchronize]
|
212
|
+
synchronize( options[:synchronize] )
|
213
|
+
end
|
214
|
+
|
215
|
+
return_obj.num_inserts = 0 if return_obj.num_inserts.nil?
|
216
|
+
return_obj
|
217
|
+
end
|
218
|
+
|
219
|
+
# TODO import_from_table needs to be implemented.
|
220
|
+
def import_from_table( options ) # :nodoc:
|
221
|
+
end
|
222
|
+
|
223
|
+
# Imports the passed in +column_names+ and +array_of_attributes+
|
224
|
+
# given the passed in +options+ Hash with validations. Returns an
|
225
|
+
# object with the methods +failed_instances+ and +num_inserts+.
|
226
|
+
# +failed_instances+ is an array of instances that failed validations.
|
227
|
+
# +num_inserts+ is the number of inserts it took to import the data. See
|
228
|
+
# ActiveRecord::Base.import for more information on
|
229
|
+
# +column_names+, +array_of_attributes+ and +options+.
|
230
|
+
def import_with_validations( column_names, array_of_attributes, options={} )
|
231
|
+
failed_instances = []
|
232
|
+
|
233
|
+
# create instances for each of our column/value sets
|
234
|
+
arr = validations_array_for_column_names_and_attributes( column_names, array_of_attributes )
|
235
|
+
|
236
|
+
# keep track of the instance and the position it is currently at. if this fails
|
237
|
+
# validation we'll use the index to remove it from the array_of_attributes
|
238
|
+
arr.each_with_index do |hsh,i|
|
239
|
+
instance = new( hsh )
|
240
|
+
if not instance.valid?
|
241
|
+
array_of_attributes[ i ] = nil
|
242
|
+
failed_instances << instance
|
243
|
+
end
|
244
|
+
end
|
245
|
+
array_of_attributes.compact!
|
246
|
+
|
247
|
+
num_inserts = array_of_attributes.empty? ? 0 : import_without_validations_or_callbacks( column_names, array_of_attributes, options )
|
248
|
+
ActiveRecord::Import::Result.new(failed_instances, num_inserts)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Imports the passed in +column_names+ and +array_of_attributes+
|
252
|
+
# given the passed in +options+ Hash. This will return the number
|
253
|
+
# of insert operations it took to create these records without
|
254
|
+
# validations or callbacks. See ActiveRecord::Base.import for more
|
255
|
+
# information on +column_names+, +array_of_attributes_ and
|
256
|
+
# +options+.
|
257
|
+
def import_without_validations_or_callbacks( column_names, array_of_attributes, options={} )
|
258
|
+
columns_sql = "(#{column_names.map{|name| connection.quote_column_name(name) }.join(',')})"
|
259
|
+
insert_sql = "INSERT #{options[:ignore] ? 'IGNORE ':''}INTO #{quoted_table_name} #{columns_sql} VALUES "
|
260
|
+
values_sql = values_sql_for_column_names_and_attributes(column_names, array_of_attributes)
|
261
|
+
if not supports_import?
|
262
|
+
number_inserted = 0
|
263
|
+
values_sql.each do |values|
|
264
|
+
connection.execute(insert_sql + values)
|
265
|
+
number_inserted += 1
|
266
|
+
end
|
267
|
+
else
|
268
|
+
# generate the sql
|
269
|
+
post_sql_statements = connection.post_sql_statements( quoted_table_name, options )
|
270
|
+
|
271
|
+
# perform the inserts
|
272
|
+
number_inserted = connection.insert_many( [ insert_sql, post_sql_statements ].flatten,
|
273
|
+
values_sql,
|
274
|
+
"#{self.class.name} Create Many Without Validations Or Callbacks" )
|
275
|
+
end
|
276
|
+
number_inserted
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
# Returns SQL the VALUES for an INSERT statement given the passed in +columns+
|
282
|
+
# and +array_of_attributes+.
|
283
|
+
def values_sql_for_column_names_and_attributes(column_names, array_of_attributes) # :nodoc:
|
284
|
+
columns = column_names.map { |name| columns_hash[name] }
|
285
|
+
|
286
|
+
array_of_attributes.map do |arr|
|
287
|
+
my_values = arr.each_with_index.map do |val,j|
|
288
|
+
if !sequence_name.blank? && column_names[j] == primary_key && val.nil?
|
289
|
+
connection.next_value_for_sequence(sequence_name)
|
290
|
+
else
|
291
|
+
connection.quote(val, columns[j])
|
292
|
+
end
|
293
|
+
end
|
294
|
+
"(#{my_values.join(',')})"
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def add_special_rails_stamps( column_names, array_of_attributes, options )
|
299
|
+
AREXT_RAILS_COLUMNS[:create].each_pair do |key, blk|
|
300
|
+
if self.column_names.include?(key)
|
301
|
+
value = blk.call
|
302
|
+
if index=column_names.index(key)
|
303
|
+
# replace every instance of the array of attributes with our value
|
304
|
+
array_of_attributes.each{ |arr| arr[index] = value }
|
305
|
+
else
|
306
|
+
column_names << key
|
307
|
+
array_of_attributes.each { |arr| arr << value }
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
AREXT_RAILS_COLUMNS[:update].each_pair do |key, blk|
|
313
|
+
if self.column_names.include?(key)
|
314
|
+
value = blk.call
|
315
|
+
if index=column_names.index(key)
|
316
|
+
# replace every instance of the array of attributes with our value
|
317
|
+
array_of_attributes.each{ |arr| arr[index] = value }
|
318
|
+
else
|
319
|
+
column_names << key
|
320
|
+
array_of_attributes.each { |arr| arr << value }
|
321
|
+
end
|
322
|
+
|
323
|
+
if supports_on_duplicate_key_update?
|
324
|
+
if options[:on_duplicate_key_update]
|
325
|
+
options[:on_duplicate_key_update] << key.to_sym if options[:on_duplicate_key_update].is_a?(Array)
|
326
|
+
options[:on_duplicate_key_update][key.to_sym] = key.to_sym if options[:on_duplicate_key_update].is_a?(Hash)
|
327
|
+
else
|
328
|
+
options[:on_duplicate_key_update] = [ key.to_sym ]
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Returns an Array of Hashes for the passed in +column_names+ and +array_of_attributes+.
|
336
|
+
def validations_array_for_column_names_and_attributes( column_names, array_of_attributes ) # :nodoc:
|
337
|
+
arr = []
|
338
|
+
array_of_attributes.each do |attributes|
|
339
|
+
c = 0
|
340
|
+
hsh = attributes.inject( {} ){|hsh,attr| hsh[ column_names[c] ] = attr ; c+=1 ; hsh }
|
341
|
+
arr << hsh
|
342
|
+
end
|
343
|
+
arr
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
end
|