activerecord-import-rails4 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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