activerecord-import-uuid 0.1

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 (98) hide show
  1. data/.gitignore +32 -0
  2. data/.rubocop.yml +49 -0
  3. data/.rubocop_todo.yml +36 -0
  4. data/.travis.yml +52 -0
  5. data/Brewfile +3 -0
  6. data/CHANGELOG.md +87 -0
  7. data/Gemfile +54 -0
  8. data/LICENSE +56 -0
  9. data/README.markdown +101 -0
  10. data/Rakefile +66 -0
  11. data/activerecord-import.gemspec +23 -0
  12. data/benchmarks/README +32 -0
  13. data/benchmarks/benchmark.rb +67 -0
  14. data/benchmarks/lib/base.rb +138 -0
  15. data/benchmarks/lib/cli_parser.rb +106 -0
  16. data/benchmarks/lib/float.rb +15 -0
  17. data/benchmarks/lib/mysql2_benchmark.rb +19 -0
  18. data/benchmarks/lib/output_to_csv.rb +19 -0
  19. data/benchmarks/lib/output_to_html.rb +64 -0
  20. data/benchmarks/models/test_innodb.rb +3 -0
  21. data/benchmarks/models/test_memory.rb +3 -0
  22. data/benchmarks/models/test_myisam.rb +3 -0
  23. data/benchmarks/schema/mysql_schema.rb +16 -0
  24. data/gemfiles/3.2.gemfile +3 -0
  25. data/gemfiles/4.0.gemfile +3 -0
  26. data/gemfiles/4.1.gemfile +3 -0
  27. data/gemfiles/4.2.gemfile +7 -0
  28. data/gemfiles/5.0.gemfile +3 -0
  29. data/lib/activerecord-import.rb +19 -0
  30. data/lib/activerecord-import/active_record/adapters/abstract_adapter.rb +9 -0
  31. data/lib/activerecord-import/active_record/adapters/jdbcmysql_adapter.rb +6 -0
  32. data/lib/activerecord-import/active_record/adapters/jdbcpostgresql_adapter.rb +6 -0
  33. data/lib/activerecord-import/active_record/adapters/mysql2_adapter.rb +6 -0
  34. data/lib/activerecord-import/active_record/adapters/postgresql_adapter.rb +6 -0
  35. data/lib/activerecord-import/active_record/adapters/seamless_database_pool_adapter.rb +7 -0
  36. data/lib/activerecord-import/active_record/adapters/sqlite3_adapter.rb +6 -0
  37. data/lib/activerecord-import/adapters/abstract_adapter.rb +78 -0
  38. data/lib/activerecord-import/adapters/em_mysql2_adapter.rb +5 -0
  39. data/lib/activerecord-import/adapters/mysql2_adapter.rb +5 -0
  40. data/lib/activerecord-import/adapters/mysql_adapter.rb +114 -0
  41. data/lib/activerecord-import/adapters/postgresql_adapter.rb +144 -0
  42. data/lib/activerecord-import/adapters/sqlite3_adapter.rb +51 -0
  43. data/lib/activerecord-import/base.rb +38 -0
  44. data/lib/activerecord-import/import.rb +660 -0
  45. data/lib/activerecord-import/mysql2.rb +7 -0
  46. data/lib/activerecord-import/postgresql.rb +7 -0
  47. data/lib/activerecord-import/sqlite3.rb +7 -0
  48. data/lib/activerecord-import/synchronize.rb +66 -0
  49. data/lib/activerecord-import/value_sets_parser.rb +55 -0
  50. data/lib/activerecord-import/version.rb +5 -0
  51. data/test/adapters/jdbcmysql.rb +1 -0
  52. data/test/adapters/jdbcpostgresql.rb +1 -0
  53. data/test/adapters/mysql2.rb +1 -0
  54. data/test/adapters/mysql2_makara.rb +1 -0
  55. data/test/adapters/mysql2spatial.rb +1 -0
  56. data/test/adapters/postgis.rb +1 -0
  57. data/test/adapters/postgresql.rb +1 -0
  58. data/test/adapters/postgresql_makara.rb +1 -0
  59. data/test/adapters/seamless_database_pool.rb +1 -0
  60. data/test/adapters/spatialite.rb +1 -0
  61. data/test/adapters/sqlite3.rb +1 -0
  62. data/test/database.yml.sample +52 -0
  63. data/test/import_test.rb +574 -0
  64. data/test/jdbcmysql/import_test.rb +6 -0
  65. data/test/jdbcpostgresql/import_test.rb +5 -0
  66. data/test/models/book.rb +7 -0
  67. data/test/models/chapter.rb +4 -0
  68. data/test/models/discount.rb +3 -0
  69. data/test/models/end_note.rb +4 -0
  70. data/test/models/group.rb +3 -0
  71. data/test/models/promotion.rb +3 -0
  72. data/test/models/question.rb +3 -0
  73. data/test/models/rule.rb +3 -0
  74. data/test/models/topic.rb +9 -0
  75. data/test/models/widget.rb +24 -0
  76. data/test/mysql2/import_test.rb +5 -0
  77. data/test/mysql2_makara/import_test.rb +6 -0
  78. data/test/mysqlspatial2/import_test.rb +6 -0
  79. data/test/postgis/import_test.rb +4 -0
  80. data/test/postgresql/import_test.rb +8 -0
  81. data/test/schema/generic_schema.rb +144 -0
  82. data/test/schema/mysql_schema.rb +16 -0
  83. data/test/schema/version.rb +10 -0
  84. data/test/sqlite3/import_test.rb +52 -0
  85. data/test/support/active_support/test_case_extensions.rb +70 -0
  86. data/test/support/assertions.rb +73 -0
  87. data/test/support/factories.rb +57 -0
  88. data/test/support/generate.rb +29 -0
  89. data/test/support/mysql/import_examples.rb +85 -0
  90. data/test/support/postgresql/import_examples.rb +242 -0
  91. data/test/support/shared_examples/on_duplicate_key_update.rb +103 -0
  92. data/test/support/shared_examples/recursive_import.rb +122 -0
  93. data/test/synchronize_test.rb +33 -0
  94. data/test/test_helper.rb +59 -0
  95. data/test/travis/database.yml +62 -0
  96. data/test/value_sets_bytes_parser_test.rb +93 -0
  97. data/test/value_sets_records_parser_test.rb +32 -0
  98. metadata +225 -0
@@ -0,0 +1,19 @@
1
+ class Mysql2Benchmark < BenchmarkBase
2
+ def benchmark_all( array_of_cols_and_vals )
3
+ methods = self.methods.find_all { |m| m =~ /benchmark_/ }
4
+ methods.delete_if { |m| m =~ /benchmark_(all|model)/ }
5
+ methods.each { |method| send( method, array_of_cols_and_vals ) }
6
+ end
7
+
8
+ def benchmark_myisam( array_of_cols_and_vals )
9
+ bm_model( TestMyISAM, array_of_cols_and_vals )
10
+ end
11
+
12
+ def benchmark_innodb( array_of_cols_and_vals )
13
+ bm_model( TestInnoDb, array_of_cols_and_vals )
14
+ end
15
+
16
+ def benchmark_memory( array_of_cols_and_vals )
17
+ bm_model( TestMemory, array_of_cols_and_vals )
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'csv'
2
+
3
+ module OutputToCSV
4
+ def self.output_results( filename, results )
5
+ CSV.open( filename, 'w' ) do |csv|
6
+ # Iterate over each result set, which contains many results
7
+ results.each do |result_set|
8
+ columns = []
9
+ times = []
10
+ result_set.each do |result|
11
+ columns << result.description
12
+ times << result.tms.real
13
+ end
14
+ csv << columns
15
+ csv << times
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ require 'erb'
2
+
3
+ module OutputToHTML
4
+ TEMPLATE_HEADER = <<"EOT".freeze
5
+ <div>
6
+ All times are rounded to the nearest thousandth for display purposes. Speedups next to each time are computed
7
+ before any rounding occurs. Also, all speedup calculations are computed by comparing a given time against
8
+ the very first column (which is always the default ActiveRecord::Base.create method.
9
+ </div>
10
+ EOT
11
+
12
+ TEMPLATE = <<"EOT".freeze
13
+ <style>
14
+ td#benchmarkTitle {
15
+ border: 1px solid black;
16
+ padding: 2px;
17
+ font-size: 0.8em;
18
+ background-color: black;
19
+ color: white;
20
+ }
21
+ td#benchmarkCell {
22
+ border: 1px solid black;
23
+ padding: 2px;
24
+ font-size: 0.8em;
25
+ }
26
+ </style>
27
+ <table>
28
+ <tr>
29
+ <% columns.each do |col| %>
30
+ <td id="benchmarkTitle"><%= col %></td>
31
+ <% end %>
32
+ </tr>
33
+ <tr>
34
+ <% times.each do |time| %>
35
+ <td id="benchmarkCell"><%= time %></td>
36
+ <% end %>
37
+ </tr>
38
+ <tr><td>&nbsp;</td></tr>
39
+ </table>
40
+ EOT
41
+
42
+ def self.output_results( filename, results )
43
+ html = ''
44
+ results.each do |result_set|
45
+ columns = []
46
+ 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
+ times << (result == result_set.first ? time.to_s : "#{time} (#{speedup}x speedup)")
55
+ end
56
+ end
57
+
58
+ template = ERB.new( TEMPLATE, 0, "%<>")
59
+ html << template.result( binding )
60
+ end
61
+
62
+ File.open( filename, 'w' ) { |file| file.write( TEMPLATE_HEADER + html ) }
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ class TestInnoDb < ActiveRecord::Base
2
+ self.table_name = 'test_innodb'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMemory < ActiveRecord::Base
2
+ self.table_name = 'test_memory'
3
+ end
@@ -0,0 +1,3 @@
1
+ class TestMyISAM < ActiveRecord::Base
2
+ self.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,3 @@
1
+ platforms :ruby do
2
+ gem 'activerecord', '~> 3.2.0'
3
+ end
@@ -0,0 +1,3 @@
1
+ platforms :ruby do
2
+ gem 'activerecord', '~> 4.0.0'
3
+ end
@@ -0,0 +1,3 @@
1
+ platforms :ruby do
2
+ gem 'activerecord', '~> 4.1.0'
3
+ end
@@ -0,0 +1,7 @@
1
+ platforms :ruby do
2
+ gem 'activerecord', '~> 4.2.0'
3
+ end
4
+
5
+ platforms :jruby do
6
+ gem 'activerecord', '~> 4.2.0'
7
+ end
@@ -0,0 +1,3 @@
1
+ platforms :ruby do
2
+ gem 'activerecord', '~> 5.0.0'
3
+ end
@@ -0,0 +1,19 @@
1
+ # rubocop:disable Style/FileName
2
+
3
+ ActiveSupport.on_load(:active_record) do
4
+ class ActiveRecord::Base
5
+ class << self
6
+ def establish_connection_with_activerecord_import(*args)
7
+ conn = establish_connection_without_activerecord_import(*args)
8
+ if !ActiveRecord.const_defined?(:Import) || !ActiveRecord::Import.respond_to?(:load_from_connection_pool)
9
+ require "activerecord-import/base"
10
+ end
11
+
12
+ ActiveRecord::Import.load_from_connection_pool connection_pool
13
+ conn
14
+ end
15
+ alias establish_connection_without_activerecord_import establish_connection
16
+ alias establish_connection establish_connection_with_activerecord_import
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,9 @@
1
+ require "activerecord-import/adapters/abstract_adapter"
2
+
3
+ module ActiveRecord # :nodoc:
4
+ module ConnectionAdapters # :nodoc:
5
+ class AbstractAdapter # :nodoc:
6
+ include ActiveRecord::Import::AbstractAdapter::InstanceMethods
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql_adapter"
2
+ require "activerecord-import/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/postgresql_adapter"
2
+ require "activerecord-import/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter
6
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/mysql2_adapter"
2
+ require "activerecord-import/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/postgresql_adapter"
2
+ require "activerecord-import/adapters/postgresql_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
+ include ActiveRecord::Import::PostgreSQLAdapter
6
+ end
@@ -0,0 +1,7 @@
1
+ require "seamless_database_pool"
2
+ require "active_record/connection_adapters/seamless_database_pool_adapter"
3
+ require "activerecord-import/adapters/mysql_adapter"
4
+
5
+ class ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter
6
+ include ActiveRecord::Import::MysqlAdapter
7
+ end
@@ -0,0 +1,6 @@
1
+ require "active_record/connection_adapters/sqlite3_adapter"
2
+ require "activerecord-import/adapters/sqlite3_adapter"
3
+
4
+ class ActiveRecord::ConnectionAdapters::SQLite3Adapter
5
+ include ActiveRecord::Import::SQLite3Adapter
6
+ end
@@ -0,0 +1,78 @@
1
+ module ActiveRecord::Import::AbstractAdapter
2
+ module InstanceMethods
3
+ def next_value_for_sequence(sequence_name)
4
+ %(#{sequence_name}.nextval)
5
+ end
6
+
7
+ def insert_many( sql, values, *args ) # :nodoc:
8
+ number_of_inserts = 1
9
+
10
+ base_sql, post_sql = if sql.is_a?( String )
11
+ [sql, '']
12
+ elsif sql.is_a?( Array )
13
+ [sql.shift, sql.join( ' ' )]
14
+ end
15
+
16
+ sql2insert = base_sql + values.join( ',' ) + post_sql
17
+ insert( sql2insert, *args )
18
+
19
+ [number_of_inserts, []]
20
+ end
21
+
22
+ def pre_sql_statements(options)
23
+ sql = []
24
+ sql << options[:pre_sql] if options[:pre_sql]
25
+ sql << options[:command] if options[:command]
26
+ sql << "IGNORE" if options[:ignore]
27
+
28
+ # add keywords like IGNORE or DELAYED
29
+ if options[:keywords].is_a?(Array)
30
+ sql.concat(options[:keywords])
31
+ elsif options[:keywords]
32
+ sql << options[:keywords].to_s
33
+ end
34
+
35
+ sql
36
+ end
37
+
38
+ # Synchronizes the passed in ActiveRecord instances with the records in
39
+ # the database by calling +reload+ on each instance.
40
+ def after_import_synchronize( instances )
41
+ instances.each(&:reload)
42
+ end
43
+
44
+ # Returns an array of post SQL statements given the passed in options.
45
+ def post_sql_statements( table_name, options ) # :nodoc:
46
+ post_sql_statements = []
47
+
48
+ if supports_on_duplicate_key_update?
49
+ if options[:on_duplicate_key_ignore] && respond_to?(:sql_for_on_duplicate_key_ignore)
50
+ # Options :recursive and :on_duplicate_key_ignore are mutually exclusive
51
+ unless options[:recursive]
52
+ post_sql_statements << sql_for_on_duplicate_key_ignore( table_name, options[:on_duplicate_key_ignore] )
53
+ end
54
+ elsif options[:on_duplicate_key_update]
55
+ post_sql_statements << sql_for_on_duplicate_key_update( table_name, options[:on_duplicate_key_update] )
56
+ end
57
+ end
58
+
59
+ # custom user post_sql
60
+ post_sql_statements << options[:post_sql] if options[:post_sql]
61
+
62
+ # with rollup
63
+ post_sql_statements << rollup_sql if options[:rollup]
64
+
65
+ post_sql_statements
66
+ end
67
+
68
+ # Returns the maximum number of bytes that the server will allow
69
+ # in a single packet
70
+ def max_allowed_packet
71
+ NO_MAX_PACKET
72
+ end
73
+
74
+ def supports_on_duplicate_key_update?
75
+ false
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ require "activerecord-import/adapters/mysql_adapter"
2
+
3
+ module ActiveRecord::Import::EMMysql2Adapter
4
+ include ActiveRecord::Import::MysqlAdapter
5
+ end
@@ -0,0 +1,5 @@
1
+ require "activerecord-import/adapters/mysql_adapter"
2
+
3
+ module ActiveRecord::Import::Mysql2Adapter
4
+ include ActiveRecord::Import::MysqlAdapter
5
+ end
@@ -0,0 +1,114 @@
1
+ module ActiveRecord::Import::MysqlAdapter
2
+ include ActiveRecord::Import::ImportSupport
3
+ include ActiveRecord::Import::OnDuplicateKeyUpdateSupport
4
+
5
+ NO_MAX_PACKET = 0
6
+ QUERY_OVERHEAD = 8 # This was shown to be true for MySQL, but it's not clear where the overhead is from.
7
+
8
+ # +sql+ can be a single string or an array. If it is an array all
9
+ # elements that are in position >= 1 will be appended to the final SQL.
10
+ def insert_many( sql, values, *args ) # :nodoc:
11
+ # the number of inserts default
12
+ number_of_inserts = 0
13
+
14
+ base_sql, post_sql = if sql.is_a?( String )
15
+ [sql, '']
16
+ elsif sql.is_a?( Array )
17
+ [sql.shift, sql.join( ' ' )]
18
+ end
19
+
20
+ sql_size = QUERY_OVERHEAD + base_sql.size + post_sql.size
21
+
22
+ # the number of bytes the requested insert statement values will take up
23
+ values_in_bytes = values.sum(&:bytesize)
24
+
25
+ # the number of bytes (commas) it will take to comma separate our values
26
+ comma_separated_bytes = values.size - 1
27
+
28
+ # the total number of bytes required if this statement is one statement
29
+ total_bytes = sql_size + values_in_bytes + comma_separated_bytes
30
+
31
+ max = max_allowed_packet
32
+
33
+ # if we can insert it all as one statement
34
+ if NO_MAX_PACKET == max || total_bytes < max
35
+ number_of_inserts += 1
36
+ sql2insert = base_sql + values.join( ',' ) + post_sql
37
+ insert( sql2insert, *args )
38
+ else
39
+ value_sets = ::ActiveRecord::Import::ValueSetsBytesParser.parse(values,
40
+ reserved_bytes: sql_size,
41
+ max_bytes: max)
42
+ value_sets.each do |value_set|
43
+ number_of_inserts += 1
44
+ sql2insert = base_sql + value_set.join( ',' ) + post_sql
45
+ insert( sql2insert, *args )
46
+ end
47
+ end
48
+
49
+ [number_of_inserts, []]
50
+ end
51
+
52
+ # Returns the maximum number of bytes that the server will allow
53
+ # in a single packet
54
+ def max_allowed_packet # :nodoc:
55
+ @max_allowed_packet ||= begin
56
+ result = execute( "SHOW VARIABLES like 'max_allowed_packet';" )
57
+ # original Mysql gem responds to #fetch_row while Mysql2 responds to #first
58
+ val = result.respond_to?(:fetch_row) ? result.fetch_row[1] : result.first[1]
59
+ val.to_i
60
+ end
61
+ end
62
+
63
+ # Add a column to be updated on duplicate key update
64
+ def add_column_for_on_duplicate_key_update( column, options = {} ) # :nodoc:
65
+ if options.include?(:on_duplicate_key_update)
66
+ columns = options[:on_duplicate_key_update]
67
+ case columns
68
+ when Array then columns << column.to_sym unless columns.include?(column.to_sym)
69
+ when Hash then columns[column.to_sym] = column.to_sym
70
+ end
71
+ else
72
+ options[:on_duplicate_key_update] = [column.to_sym]
73
+ end
74
+ end
75
+
76
+ # Returns a generated ON DUPLICATE KEY UPDATE statement given the passed
77
+ # in +args+.
78
+ def sql_for_on_duplicate_key_update( table_name, *args ) # :nodoc:
79
+ sql = ' ON DUPLICATE KEY UPDATE '
80
+ arg = args.first
81
+ if arg.is_a?( Array )
82
+ sql << sql_for_on_duplicate_key_update_as_array( table_name, arg )
83
+ elsif arg.is_a?( Hash )
84
+ sql << sql_for_on_duplicate_key_update_as_hash( table_name, arg )
85
+ elsif arg.is_a?( String )
86
+ sql << arg
87
+ else
88
+ raise ArgumentError, "Expected Array or Hash"
89
+ end
90
+ sql
91
+ end
92
+
93
+ def sql_for_on_duplicate_key_update_as_array( table_name, arr ) # :nodoc:
94
+ results = arr.map do |column|
95
+ qc = quote_column_name( column )
96
+ "#{table_name}.#{qc}=VALUES(#{qc})"
97
+ end
98
+ results.join( ',' )
99
+ end
100
+
101
+ def sql_for_on_duplicate_key_update_as_hash( table_name, hsh ) # :nodoc:
102
+ results = hsh.map do |column1, column2|
103
+ qc1 = quote_column_name( column1 )
104
+ qc2 = quote_column_name( column2 )
105
+ "#{table_name}.#{qc1}=VALUES( #{qc2} )"
106
+ end
107
+ results.join( ',')
108
+ end
109
+
110
+ # Return true if the statement is a duplicate key record error
111
+ def duplicate_key_update_error?(exception) # :nodoc:
112
+ exception.is_a?(ActiveRecord::StatementInvalid) && exception.to_s.include?('Duplicate entry')
113
+ end
114
+ end