activerecord-import-rails4 0.5.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 +7 -0
- data/.gitignore +31 -0
- data/Appraisals +9 -0
- data/Gemfile +25 -0
- data/README.markdown +24 -0
- data/Rakefile +52 -0
- data/activerecord-import-rails4.gemspec +24 -0
- data/benchmarks/README +32 -0
- data/benchmarks/benchmark.rb +64 -0
- data/benchmarks/boot.rb +18 -0
- data/benchmarks/lib/base.rb +137 -0
- data/benchmarks/lib/cli_parser.rb +103 -0
- data/benchmarks/lib/float.rb +15 -0
- data/benchmarks/lib/mysql_benchmark.rb +22 -0
- data/benchmarks/lib/output_to_csv.rb +18 -0
- data/benchmarks/lib/output_to_html.rb +69 -0
- data/benchmarks/models/test_innodb.rb +3 -0
- data/benchmarks/models/test_memory.rb +3 -0
- data/benchmarks/models/test_myisam.rb +3 -0
- data/benchmarks/schema/mysql_schema.rb +16 -0
- data/gemfiles/rails3.gemfile +18 -0
- data/gemfiles/rails4.gemfile +18 -0
- data/lib/activerecord-import-rails4.rb +16 -0
- data/lib/activerecord-import-rails4/active_record/adapters/abstract_adapter.rb +10 -0
- data/lib/activerecord-import-rails4/active_record/adapters/jdbcmysql_adapter.rb +6 -0
- data/lib/activerecord-import-rails4/active_record/adapters/mysql2_adapter.rb +6 -0
- data/lib/activerecord-import-rails4/active_record/adapters/mysql_adapter.rb +6 -0
- data/lib/activerecord-import-rails4/active_record/adapters/postgresql_adapter.rb +7 -0
- data/lib/activerecord-import-rails4/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
- data/lib/activerecord-import-rails4/active_record/adapters/sqlite3_adapter.rb +7 -0
- data/lib/activerecord-import-rails4/adapters/abstract_adapter.rb +119 -0
- data/lib/activerecord-import-rails4/adapters/mysql2_adapter.rb +5 -0
- data/lib/activerecord-import-rails4/adapters/mysql_adapter.rb +55 -0
- data/lib/activerecord-import-rails4/adapters/postgresql_adapter.rb +7 -0
- data/lib/activerecord-import-rails4/adapters/sqlite3_adapter.rb +5 -0
- data/lib/activerecord-import-rails4/base.rb +34 -0
- data/lib/activerecord-import-rails4/import.rb +387 -0
- data/lib/activerecord-import-rails4/mysql.rb +8 -0
- data/lib/activerecord-import-rails4/mysql2.rb +8 -0
- data/lib/activerecord-import-rails4/postgresql.rb +8 -0
- data/lib/activerecord-import-rails4/sqlite3.rb +8 -0
- data/lib/activerecord-import-rails4/synchronize.rb +60 -0
- data/lib/activerecord-import-rails4/version.rb +5 -0
- data/test/active_record/connection_adapter_test.rb +62 -0
- data/test/adapters/jdbcmysql.rb +1 -0
- data/test/adapters/mysql.rb +1 -0
- data/test/adapters/mysql2.rb +1 -0
- data/test/adapters/mysql2spatial.rb +1 -0
- data/test/adapters/mysqlspatial.rb +1 -0
- data/test/adapters/postgis.rb +1 -0
- data/test/adapters/postgresql.rb +1 -0
- data/test/adapters/seamless_database_pool.rb +1 -0
- data/test/adapters/spatialite.rb +1 -0
- data/test/adapters/sqlite3.rb +1 -0
- data/test/import_test.rb +321 -0
- data/test/jdbcmysql/import_test.rb +6 -0
- data/test/models/book.rb +3 -0
- data/test/models/group.rb +3 -0
- data/test/models/topic.rb +7 -0
- data/test/models/widget.rb +3 -0
- data/test/mysql/import_test.rb +6 -0
- data/test/mysql2/import_test.rb +6 -0
- data/test/mysqlspatial/import_test.rb +6 -0
- data/test/mysqlspatial2/import_test.rb +6 -0
- data/test/postgis/import_test.rb +4 -0
- data/test/postgresql/import_test.rb +4 -0
- data/test/schema/generic_schema.rb +102 -0
- data/test/schema/mysql_schema.rb +17 -0
- data/test/schema/version.rb +10 -0
- data/test/support/active_support/test_case_extensions.rb +67 -0
- data/test/support/factories.rb +19 -0
- data/test/support/generate.rb +29 -0
- data/test/support/mysql/assertions.rb +55 -0
- data/test/support/mysql/import_examples.rb +190 -0
- data/test/support/postgresql/import_examples.rb +21 -0
- data/test/synchronize_test.rb +22 -0
- data/test/test_helper.rb +48 -0
- metadata +197 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# Taken from http://www.programmingishard.com/posts/show/128
|
2
|
+
# Posted by rbates
|
3
|
+
class Float
|
4
|
+
def round_to(x)
|
5
|
+
(self * 10**x).round.to_f / 10**x
|
6
|
+
end
|
7
|
+
|
8
|
+
def ceil_to(x)
|
9
|
+
(self * 10**x).ceil.to_f / 10**x
|
10
|
+
end
|
11
|
+
|
12
|
+
def floor_to(x)
|
13
|
+
(self * 10**x).floor.to_f / 10**x
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class MysqlBenchmark < BenchmarkBase
|
2
|
+
|
3
|
+
def benchmark_all( array_of_cols_and_vals )
|
4
|
+
methods = self.methods.find_all { |m| m =~ /benchmark_/ }
|
5
|
+
methods.delete_if{ |m| m =~ /benchmark_(all|model)/ }
|
6
|
+
methods.each { |method| self.send( method, array_of_cols_and_vals ) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def benchmark_myisam( array_of_cols_and_vals )
|
10
|
+
bm_model( TestMyISAM, array_of_cols_and_vals )
|
11
|
+
end
|
12
|
+
|
13
|
+
def benchmark_innodb( array_of_cols_and_vals )
|
14
|
+
bm_model( TestInnoDb, array_of_cols_and_vals )
|
15
|
+
end
|
16
|
+
|
17
|
+
def benchmark_memory( array_of_cols_and_vals )
|
18
|
+
bm_model( TestMemory, array_of_cols_and_vals )
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'fastercsv'
|
2
|
+
|
3
|
+
module OutputToCSV
|
4
|
+
def self.output_results( filename, results )
|
5
|
+
FasterCSV.open( filename, 'w' ) do |csv|
|
6
|
+
# Iterate over each result set, which contains many results
|
7
|
+
results.each do |result_set|
|
8
|
+
columns, times = [], []
|
9
|
+
result_set.each do |result|
|
10
|
+
columns << result.description
|
11
|
+
times << result.tms.real
|
12
|
+
end
|
13
|
+
csv << columns
|
14
|
+
csv << times
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module OutputToHTML
|
4
|
+
|
5
|
+
TEMPLATE_HEADER =<<"EOT"
|
6
|
+
<div>
|
7
|
+
All times are rounded to the nearest thousandth for display purposes. Speedups next to each time are computed
|
8
|
+
before any rounding occurs. Also, all speedup calculations are computed by comparing a given time against
|
9
|
+
the very first column (which is always the default ActiveRecord::Base.create method.
|
10
|
+
</div>
|
11
|
+
EOT
|
12
|
+
|
13
|
+
TEMPLATE =<<"EOT"
|
14
|
+
<style>
|
15
|
+
td#benchmarkTitle {
|
16
|
+
border: 1px solid black;
|
17
|
+
padding: 2px;
|
18
|
+
font-size: 0.8em;
|
19
|
+
background-color: black;
|
20
|
+
color: white;
|
21
|
+
}
|
22
|
+
td#benchmarkCell {
|
23
|
+
border: 1px solid black;
|
24
|
+
padding: 2px;
|
25
|
+
font-size: 0.8em;
|
26
|
+
}
|
27
|
+
</style>
|
28
|
+
<table>
|
29
|
+
<tr>
|
30
|
+
<% columns.each do |col| %>
|
31
|
+
<td id="benchmarkTitle"><%= col %></td>
|
32
|
+
<% end %>
|
33
|
+
</tr>
|
34
|
+
<tr>
|
35
|
+
<% times.each do |time| %>
|
36
|
+
<td id="benchmarkCell"><%= time %></td>
|
37
|
+
<% end %>
|
38
|
+
</tr>
|
39
|
+
<tr><td> </td></tr>
|
40
|
+
</table>
|
41
|
+
EOT
|
42
|
+
|
43
|
+
def self.output_results( filename, results )
|
44
|
+
html = ''
|
45
|
+
results.each do |result_set|
|
46
|
+
columns, times = [], []
|
47
|
+
result_set.each do |result|
|
48
|
+
columns << result.description
|
49
|
+
if result.failed
|
50
|
+
times << "failed"
|
51
|
+
else
|
52
|
+
time = result.tms.real.round_to( 3 )
|
53
|
+
speedup = ( result_set.first.tms.real / result.tms.real ).round
|
54
|
+
|
55
|
+
if result == result_set.first
|
56
|
+
times << "#{time}"
|
57
|
+
else
|
58
|
+
times << "#{time} (#{speedup}x speedup)"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
template = ERB.new( TEMPLATE, 0, "%<>")
|
64
|
+
html << template.result( binding )
|
65
|
+
end
|
66
|
+
|
67
|
+
File.open( filename, 'w' ){ |file| file.write( TEMPLATE_HEADER + html ) }
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
ActiveRecord::Schema.define do
|
2
|
+
create_table :test_myisam, :options=>'ENGINE=MyISAM', :force=>true do |t|
|
3
|
+
t.column :my_name, :string, :null=>false
|
4
|
+
t.column :description, :string
|
5
|
+
end
|
6
|
+
|
7
|
+
create_table :test_innodb, :options=>'ENGINE=InnoDb', :force=>true do |t|
|
8
|
+
t.column :my_name, :string, :null=>false
|
9
|
+
t.column :description, :string
|
10
|
+
end
|
11
|
+
|
12
|
+
create_table :test_memory, :options=>'ENGINE=Memory', :force=>true do |t|
|
13
|
+
t.column :my_name, :string, :null=>false
|
14
|
+
t.column :description, :string
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "pg", "~> 0.9", :platforms=>:ruby
|
6
|
+
gem "sqlite3-ruby", "~> 1.3.1", :platforms=>:ruby
|
7
|
+
gem "seamless_database_pool", "~> 1.0.11", :platforms=>:ruby
|
8
|
+
gem "jdbc-mysql", :platforms=>:jruby
|
9
|
+
gem "activerecord-jdbcmysql-adapter", :platforms=>:jruby
|
10
|
+
gem "factory_girl", "~> 4.2.0"
|
11
|
+
gem "delorean", "~> 0.2.0"
|
12
|
+
gem "ruby-debug-base", "= 0.10.4", :platforms=>:jruby
|
13
|
+
gem "ruby-debug", "= 0.10.4", :platforms=>:jruby
|
14
|
+
gem "debugger", :platforms=>:mri_19
|
15
|
+
gem "mysql", "~> 2.8.1", :platforms=>:ruby
|
16
|
+
gem "activerecord", "~> 3.0"
|
17
|
+
|
18
|
+
gemspec :path=>"../"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# This file was generated by Appraisal
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "pg", "~> 0.9", :platforms=>:ruby
|
6
|
+
gem "sqlite3-ruby", "~> 1.3.1", :platforms=>:ruby
|
7
|
+
gem "seamless_database_pool", "~> 1.0.11", :platforms=>:ruby
|
8
|
+
gem "jdbc-mysql", :platforms=>:jruby
|
9
|
+
gem "activerecord-jdbcmysql-adapter", :platforms=>:jruby
|
10
|
+
gem "factory_girl", "~> 4.2.0"
|
11
|
+
gem "delorean", "~> 0.2.0"
|
12
|
+
gem "ruby-debug-base", "= 0.10.4", :platforms=>:jruby
|
13
|
+
gem "ruby-debug", "= 0.10.4", :platforms=>:jruby
|
14
|
+
gem "debugger", :platforms=>:mri_19
|
15
|
+
gem "mysql", "~> 2.9", :platforms=>:ruby
|
16
|
+
gem "activerecord", "~> 4.0.0.beta1"
|
17
|
+
|
18
|
+
gemspec :path=>"../"
|
@@ -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_pool)
|
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_pool|
|
12
|
+
if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection_pool)
|
13
|
+
require File.join File.dirname(__FILE__), "activerecord-import-rails4/base"
|
14
|
+
end
|
15
|
+
ActiveRecord::Import.load_from_connection_pool connection_pool
|
16
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require "activerecord-import-rails4/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,7 @@
|
|
1
|
+
require "seamless_database_pool"
|
2
|
+
require "active_record/connection_adapters/seamless_database_pool_adapter"
|
3
|
+
require "activerecord-import-rails4/adapters/mysql_adapter"
|
4
|
+
|
5
|
+
class ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter
|
6
|
+
include ActiveRecord::Import::MysqlAdapter
|
7
|
+
end
|
@@ -0,0 +1,119 @@
|
|
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
|
+
def get_insert_value_sets( values, sql_size, max_bytes ) # :nodoc:
|
7
|
+
value_sets = []
|
8
|
+
arr, current_arr_values_size, current_size = [], 0, 0
|
9
|
+
values.each_with_index do |val,i|
|
10
|
+
comma_bytes = arr.size
|
11
|
+
sql_size_thus_far = sql_size + current_size + val.bytesize + comma_bytes
|
12
|
+
if NO_MAX_PACKET == max_bytes or sql_size_thus_far <= max_bytes
|
13
|
+
current_size += val.bytesize
|
14
|
+
arr << val
|
15
|
+
else
|
16
|
+
value_sets << arr
|
17
|
+
arr = [ val ]
|
18
|
+
current_size = val.bytesize
|
19
|
+
end
|
20
|
+
|
21
|
+
# if we're on the last iteration push whatever we have in arr to value_sets
|
22
|
+
value_sets << arr if i == (values.size-1)
|
23
|
+
end
|
24
|
+
[ *value_sets ]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def next_value_for_sequence(sequence_name)
|
30
|
+
%{#{sequence_name}.nextval}
|
31
|
+
end
|
32
|
+
|
33
|
+
# +sql+ can be a single string or an array. If it is an array all
|
34
|
+
# elements that are in position >= 1 will be appended to the final SQL.
|
35
|
+
def insert_many( sql, values, *args ) # :nodoc:
|
36
|
+
# the number of inserts default
|
37
|
+
number_of_inserts = 0
|
38
|
+
|
39
|
+
base_sql,post_sql = if sql.is_a?( String )
|
40
|
+
[ sql, '' ]
|
41
|
+
elsif sql.is_a?( Array )
|
42
|
+
[ sql.shift, sql.join( ' ' ) ]
|
43
|
+
end
|
44
|
+
|
45
|
+
sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
|
46
|
+
|
47
|
+
# the number of bytes the requested insert statement values will take up
|
48
|
+
values_in_bytes = values.sum {|value| value.bytesize }
|
49
|
+
|
50
|
+
# the number of bytes (commas) it will take to comma separate our values
|
51
|
+
comma_separated_bytes = values.size-1
|
52
|
+
|
53
|
+
# the total number of bytes required if this statement is one statement
|
54
|
+
total_bytes = sql_size + values_in_bytes + comma_separated_bytes
|
55
|
+
|
56
|
+
max = max_allowed_packet
|
57
|
+
|
58
|
+
# if we can insert it all as one statement
|
59
|
+
if NO_MAX_PACKET == max or total_bytes < max
|
60
|
+
number_of_inserts += 1
|
61
|
+
sql2insert = base_sql + values.join( ',' ) + post_sql
|
62
|
+
insert( sql2insert, *args )
|
63
|
+
else
|
64
|
+
value_sets = self.class.get_insert_value_sets( values, sql_size, max )
|
65
|
+
value_sets.each do |values|
|
66
|
+
number_of_inserts += 1
|
67
|
+
sql2insert = base_sql + values.join( ',' ) + post_sql
|
68
|
+
insert( sql2insert, *args )
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
number_of_inserts
|
73
|
+
end
|
74
|
+
|
75
|
+
def pre_sql_statements(options)
|
76
|
+
sql = []
|
77
|
+
sql << options[:pre_sql] if options[:pre_sql]
|
78
|
+
sql << options[:command] if options[:command]
|
79
|
+
sql << "IGNORE" if options[:ignore]
|
80
|
+
|
81
|
+
#add keywords like IGNORE or DELAYED
|
82
|
+
if options[:keywords].is_a?(Array)
|
83
|
+
sql.concat(options[:keywords])
|
84
|
+
elsif options[:keywords]
|
85
|
+
sql << options[:keywords].to_s
|
86
|
+
end
|
87
|
+
|
88
|
+
sql
|
89
|
+
end
|
90
|
+
|
91
|
+
# Synchronizes the passed in ActiveRecord instances with the records in
|
92
|
+
# the database by calling +reload+ on each instance.
|
93
|
+
def after_import_synchronize( instances )
|
94
|
+
instances.each { |e| e.reload }
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns an array of post SQL statements given the passed in options.
|
98
|
+
def post_sql_statements( table_name, options ) # :nodoc:
|
99
|
+
post_sql_statements = []
|
100
|
+
if options[:on_duplicate_key_update]
|
101
|
+
post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
|
102
|
+
end
|
103
|
+
|
104
|
+
#custom user post_sql
|
105
|
+
post_sql_statements << options[:post_sql] if options[:post_sql]
|
106
|
+
|
107
|
+
#with rollup
|
108
|
+
post_sql_statements << rollup_sql if options[:rollup]
|
109
|
+
|
110
|
+
post_sql_statements
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the maximum number of bytes that the server will allow
|
114
|
+
# in a single packet
|
115
|
+
def max_allowed_packet
|
116
|
+
NO_MAX_PACKET
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module ActiveRecord::Import::MysqlAdapter
|
2
|
+
include ActiveRecord::Import::ImportSupport
|
3
|
+
include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
|
4
|
+
|
5
|
+
# Returns the maximum number of bytes that the server will allow
|
6
|
+
# in a single packet
|
7
|
+
def max_allowed_packet # :nodoc:
|
8
|
+
@max_allowed_packet ||= begin
|
9
|
+
result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
|
10
|
+
# original Mysql gem responds to #fetch_row while Mysql2 responds to #first
|
11
|
+
val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
|
12
|
+
val.to_i
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
|
17
|
+
# in +args+.
|
18
|
+
def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
|
19
|
+
sql = ' ON DUPLICATE KEY UPDATE '
|
20
|
+
arg = args.first
|
21
|
+
if arg.is_a?( Array )
|
22
|
+
sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
|
23
|
+
elsif arg.is_a?( Hash )
|
24
|
+
sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
|
25
|
+
elsif arg.is_a?( String )
|
26
|
+
sql << arg
|
27
|
+
else
|
28
|
+
raise ArgumentError.new( "Expected Array or Hash" )
|
29
|
+
end
|
30
|
+
sql
|
31
|
+
end
|
32
|
+
|
33
|
+
def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
|
34
|
+
results = arr.map do |column|
|
35
|
+
qc = quote_column_name( column )
|
36
|
+
"#{table_name}.#{qc}=VALUES(#{qc})"
|
37
|
+
end
|
38
|
+
results.join( ',' )
|
39
|
+
end
|
40
|
+
|
41
|
+
def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
|
42
|
+
sql = ' ON DUPLICATE KEY UPDATE '
|
43
|
+
results = hsh.map do |column1, column2|
|
44
|
+
qc1 = quote_column_name( column1 )
|
45
|
+
qc2 = quote_column_name( column2 )
|
46
|
+
"#{table_name}.#{qc1}=VALUES( #{qc2} )"
|
47
|
+
end
|
48
|
+
results.join( ',')
|
49
|
+
end
|
50
|
+
|
51
|
+
#return true if the statement is a duplicate key record error
|
52
|
+
def duplicate_key_update_error?(exception)# :nodoc:
|
53
|
+
exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
|
54
|
+
end
|
55
|
+
end
|