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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +31 -0
  3. data/Appraisals +9 -0
  4. data/Gemfile +25 -0
  5. data/README.markdown +24 -0
  6. data/Rakefile +52 -0
  7. data/activerecord-import-rails4.gemspec +24 -0
  8. data/benchmarks/README +32 -0
  9. data/benchmarks/benchmark.rb +64 -0
  10. data/benchmarks/boot.rb +18 -0
  11. data/benchmarks/lib/base.rb +137 -0
  12. data/benchmarks/lib/cli_parser.rb +103 -0
  13. data/benchmarks/lib/float.rb +15 -0
  14. data/benchmarks/lib/mysql_benchmark.rb +22 -0
  15. data/benchmarks/lib/output_to_csv.rb +18 -0
  16. data/benchmarks/lib/output_to_html.rb +69 -0
  17. data/benchmarks/models/test_innodb.rb +3 -0
  18. data/benchmarks/models/test_memory.rb +3 -0
  19. data/benchmarks/models/test_myisam.rb +3 -0
  20. data/benchmarks/schema/mysql_schema.rb +16 -0
  21. data/gemfiles/rails3.gemfile +18 -0
  22. data/gemfiles/rails4.gemfile +18 -0
  23. data/lib/activerecord-import-rails4.rb +16 -0
  24. data/lib/activerecord-import-rails4/active_record/adapters/abstract_adapter.rb +10 -0
  25. data/lib/activerecord-import-rails4/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  26. data/lib/activerecord-import-rails4/active_record/adapters/mysql2_adapter.rb +6 -0
  27. data/lib/activerecord-import-rails4/active_record/adapters/mysql_adapter.rb +6 -0
  28. data/lib/activerecord-import-rails4/active_record/adapters/postgresql_adapter.rb +7 -0
  29. data/lib/activerecord-import-rails4/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  30. data/lib/activerecord-import-rails4/active_record/adapters/sqlite3_adapter.rb +7 -0
  31. data/lib/activerecord-import-rails4/adapters/abstract_adapter.rb +119 -0
  32. data/lib/activerecord-import-rails4/adapters/mysql2_adapter.rb +5 -0
  33. data/lib/activerecord-import-rails4/adapters/mysql_adapter.rb +55 -0
  34. data/lib/activerecord-import-rails4/adapters/postgresql_adapter.rb +7 -0
  35. data/lib/activerecord-import-rails4/adapters/sqlite3_adapter.rb +5 -0
  36. data/lib/activerecord-import-rails4/base.rb +34 -0
  37. data/lib/activerecord-import-rails4/import.rb +387 -0
  38. data/lib/activerecord-import-rails4/mysql.rb +8 -0
  39. data/lib/activerecord-import-rails4/mysql2.rb +8 -0
  40. data/lib/activerecord-import-rails4/postgresql.rb +8 -0
  41. data/lib/activerecord-import-rails4/sqlite3.rb +8 -0
  42. data/lib/activerecord-import-rails4/synchronize.rb +60 -0
  43. data/lib/activerecord-import-rails4/version.rb +5 -0
  44. data/test/active_record/connection_adapter_test.rb +62 -0
  45. data/test/adapters/jdbcmysql.rb +1 -0
  46. data/test/adapters/mysql.rb +1 -0
  47. data/test/adapters/mysql2.rb +1 -0
  48. data/test/adapters/mysql2spatial.rb +1 -0
  49. data/test/adapters/mysqlspatial.rb +1 -0
  50. data/test/adapters/postgis.rb +1 -0
  51. data/test/adapters/postgresql.rb +1 -0
  52. data/test/adapters/seamless_database_pool.rb +1 -0
  53. data/test/adapters/spatialite.rb +1 -0
  54. data/test/adapters/sqlite3.rb +1 -0
  55. data/test/import_test.rb +321 -0
  56. data/test/jdbcmysql/import_test.rb +6 -0
  57. data/test/models/book.rb +3 -0
  58. data/test/models/group.rb +3 -0
  59. data/test/models/topic.rb +7 -0
  60. data/test/models/widget.rb +3 -0
  61. data/test/mysql/import_test.rb +6 -0
  62. data/test/mysql2/import_test.rb +6 -0
  63. data/test/mysqlspatial/import_test.rb +6 -0
  64. data/test/mysqlspatial2/import_test.rb +6 -0
  65. data/test/postgis/import_test.rb +4 -0
  66. data/test/postgresql/import_test.rb +4 -0
  67. data/test/schema/generic_schema.rb +102 -0
  68. data/test/schema/mysql_schema.rb +17 -0
  69. data/test/schema/version.rb +10 -0
  70. data/test/support/active_support/test_case_extensions.rb +67 -0
  71. data/test/support/factories.rb +19 -0
  72. data/test/support/generate.rb +29 -0
  73. data/test/support/mysql/assertions.rb +55 -0
  74. data/test/support/mysql/import_examples.rb +190 -0
  75. data/test/support/postgresql/import_examples.rb +21 -0
  76. data/test/synchronize_test.rb +22 -0
  77. data/test/test_helper.rb +48 -0
  78. 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>&nbsp;</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,3 @@
1
+ class TestInnoDb < ActiveRecord::Base
2
+ set_table_name 'test_innodb'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMemory < ActiveRecord::Base
2
+ set_table_name 'test_memory'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMyISAM < ActiveRecord::Base
2
+ set_table_name 'test_myisam'
3
+ 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,6 @@
1
+ require "active_record/connection_adapters/mysql_adapter"
2
+ require "activerecord-import-rails4/adapters/mysql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
+ include ActiveRecord::Import::MysqlAdapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import-rails4/adapters/mysql2_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
5
+ include ActiveRecord::Import::Mysql2Adapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql_adapter"
2
+ require "activerecord-import-rails4/adapters/mysql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter
5
+ include ActiveRecord::Import::MysqlAdapter
6
+ end
@@ -0,0 +1,7 @@
1
+ require "active_record/connection_adapters/postgresql_adapter"
2
+ require "activerecord-import-rails4/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter
6
+ end
7
+
@@ -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,7 @@
1
+ require "active_record/connection_adapters/sqlite3_adapter"
2
+ require "activerecord-import-rails4/adapters/sqlite3_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::SQLite3Adapter
5
+ include ActiveRecord::Import::SQLite3Adapter
6
+ end
7
+
@@ -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,5 @@
1
+ require File.dirname(__FILE__) + "/mysql_adapter"
2
+
3
+ module ActiveRecord::Import::Mysql2Adapter
4
+ include ActiveRecord::Import::MysqlAdapter
5
+ 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