upsert 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/CHANGELOG +12 -0
  2. data/README.md +6 -9
  3. data/Rakefile +9 -14
  4. data/lib/upsert.rb +40 -71
  5. data/lib/upsert/buffer.rb +36 -0
  6. data/lib/upsert/buffer/mysql2_client.rb +67 -0
  7. data/lib/upsert/buffer/pg_connection.rb +54 -0
  8. data/lib/upsert/buffer/pg_connection/merge_function.rb +138 -0
  9. data/lib/upsert/buffer/sqlite3_database.rb +13 -0
  10. data/lib/upsert/connection.rb +41 -0
  11. data/lib/upsert/connection/mysql2_client.rb +53 -0
  12. data/lib/upsert/connection/pg_connection.rb +39 -0
  13. data/lib/upsert/connection/sqlite3_database.rb +36 -0
  14. data/lib/upsert/row.rb +28 -24
  15. data/lib/upsert/version.rb +1 -1
  16. data/spec/active_record_upsert_spec.rb +16 -0
  17. data/spec/binary_spec.rb +21 -0
  18. data/spec/correctness_spec.rb +73 -0
  19. data/spec/database_functions_spec.rb +36 -0
  20. data/spec/database_spec.rb +97 -0
  21. data/spec/logger_spec.rb +37 -0
  22. data/{test → spec}/misc/get_postgres_reserved_words.rb +0 -0
  23. data/{test → spec}/misc/mysql_reserved.txt +0 -0
  24. data/{test → spec}/misc/pg_reserved.txt +0 -0
  25. data/spec/multibyte_spec.rb +27 -0
  26. data/spec/precision_spec.rb +11 -0
  27. data/spec/reserved_words_spec.rb +46 -0
  28. data/{test/helper.rb → spec/spec_helper.rb} +43 -43
  29. data/spec/speed_spec.rb +73 -0
  30. data/spec/threaded_spec.rb +34 -0
  31. data/spec/timezones_spec.rb +28 -0
  32. data/upsert.gemspec +6 -2
  33. metadata +99 -50
  34. data/lib/upsert/mysql2_client.rb +0 -104
  35. data/lib/upsert/pg_connection.rb +0 -92
  36. data/lib/upsert/pg_connection/column_definition.rb +0 -35
  37. data/lib/upsert/sqlite3_database.rb +0 -39
  38. data/test/shared/binary.rb +0 -18
  39. data/test/shared/correctness.rb +0 -72
  40. data/test/shared/database.rb +0 -94
  41. data/test/shared/multibyte.rb +0 -37
  42. data/test/shared/precision.rb +0 -8
  43. data/test/shared/reserved_words.rb +0 -45
  44. data/test/shared/speed.rb +0 -72
  45. data/test/shared/threaded.rb +0 -31
  46. data/test/shared/timezones.rb +0 -25
  47. data/test/test_active_record_connection_adapter.rb +0 -36
  48. data/test/test_active_record_upsert.rb +0 -23
  49. data/test/test_mysql2.rb +0 -43
  50. data/test/test_pg.rb +0 -45
  51. data/test/test_sqlite.rb +0 -47
@@ -1,104 +0,0 @@
1
- class Upsert
2
- # @private
3
- module Mysql2_Client
4
- def chunk
5
- return if buffer.empty?
6
- if not async?
7
- retval = sql
8
- buffer.clear
9
- return retval
10
- end
11
- @cumulative_sql_bytesize ||= static_sql_bytesize
12
- new_row = buffer.pop
13
- d = new_row.values_sql_bytesize + 3 # ),(
14
- if @cumulative_sql_bytesize + d > max_sql_bytesize
15
- retval = sql
16
- buffer.clear
17
- @cumulative_sql_bytesize = static_sql_bytesize + d
18
- else
19
- retval = nil
20
- @cumulative_sql_bytesize += d
21
- end
22
- buffer.push new_row
23
- retval
24
- end
25
-
26
- def execute(sql)
27
- connection.query sql
28
- end
29
-
30
- def columns
31
- @columns ||= buffer.first.columns
32
- end
33
-
34
- def insert_part
35
- @insert_part ||= %{INSERT INTO #{quote_ident(table_name)} (#{columns.join(',')}) VALUES }
36
- end
37
-
38
- def update_part
39
- @update_part ||= begin
40
- updaters = columns.map do |k|
41
- [ k, "VALUES(#{k})" ].join('=')
42
- end.join(',')
43
- %{ ON DUPLICATE KEY UPDATE #{updaters}}
44
- end
45
- end
46
-
47
- # where 2 is the parens
48
- def static_sql_bytesize
49
- @static_sql_bytesize ||= insert_part.bytesize + update_part.bytesize + 2
50
- end
51
-
52
- def sql
53
- all_value_sql = buffer.map { |row| row.values_sql }
54
- retval = [ insert_part, '(', all_value_sql.join('),('), ')', update_part ].join
55
- raise TooBig if retval.bytesize > max_sql_bytesize
56
- retval
57
- end
58
-
59
- # since setting an option like :as => :hash actually persists that option to the client, don't pass any options
60
- def max_sql_bytesize
61
- @max_sql_bytesize ||= database_variable_get(:MAX_ALLOWED_PACKET).to_i
62
- end
63
-
64
- def quote_boolean(v)
65
- v ? 'TRUE' : 'FALSE'
66
- end
67
-
68
- def quote_string(v)
69
- SINGLE_QUOTE + connection.escape(v) + SINGLE_QUOTE
70
- end
71
-
72
- # This doubles the size of the representation.
73
- def quote_binary(v)
74
- X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
75
- end
76
-
77
- # put raw binary straight into sql
78
- # might work if we could get the encoding issues fixed when joining together the values for the sql
79
- # alias_method :quote_binary, :quote_string
80
-
81
- def quote_time(v)
82
- quote_string v.strftime(ISO8601_DATETIME)
83
- end
84
-
85
- def quote_ident(k)
86
- BACKTICK + connection.escape(k.to_s) + BACKTICK
87
- end
88
-
89
- def quote_big_decimal(v)
90
- v.to_s('F')
91
- end
92
-
93
- def database_variable_get(k)
94
- case (row = connection.query("SHOW VARIABLES LIKE '#{k}'").first)
95
- when Array
96
- row[1]
97
- when Hash
98
- row['Value']
99
- else
100
- raise "Don't know what to do if connection.query returns a #{row.class}"
101
- end
102
- end
103
- end
104
- end
@@ -1,92 +0,0 @@
1
- require 'upsert/pg_connection/column_definition'
2
-
3
- class Upsert
4
- # @private
5
- module PG_Connection
6
-
7
- attr_reader :columns
8
- attr_reader :merge_function
9
-
10
- def chunk
11
- return if buffer.empty?
12
- row = buffer.shift
13
- unless @columns.is_a?(Array)
14
- @columns = row.columns
15
- end
16
- unless merge_function
17
- create_merge_function row
18
- end
19
- hsh = row.to_hash
20
- ordered_args = column_definitions.map do |c|
21
- hsh[c.name] || NULL_WORD
22
- end
23
- %{SELECT #{merge_function}(#{ordered_args.join(',')})}
24
- end
25
-
26
- def execute(sql)
27
- connection.exec sql
28
- end
29
-
30
- def quote_string(v)
31
- SINGLE_QUOTE + connection.escape_string(v) + SINGLE_QUOTE
32
- end
33
-
34
- def quote_binary(v)
35
- E_AND_SINGLE_QUOTE + connection.escape_bytea(v) + SINGLE_QUOTE
36
- end
37
-
38
- def quote_time(v)
39
- quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
40
- end
41
-
42
- def quote_big_decimal(v)
43
- v.to_s('F')
44
- end
45
-
46
- def quote_boolean(v)
47
- v ? 'TRUE' : 'FALSE'
48
- end
49
-
50
- def quote_ident(k)
51
- connection.quote_ident k.to_s
52
- end
53
-
54
- def column_definitions
55
- @column_definitions ||= ColumnDefinition.all(connection, table_name).select { |cd| columns.include?(cd.name) }
56
- end
57
-
58
- private
59
-
60
- def create_merge_function(example_row)
61
- @merge_function = "pg_temp.merge_#{table_name}_#{Kernel.rand(1e11)}"
62
- execute <<-EOS
63
- CREATE FUNCTION #{merge_function}(#{column_definitions.map { |c| "#{c.input_name} #{c.sql_type} DEFAULT #{c.default || 'NULL'}" }.join(',') }) RETURNS VOID AS
64
- $$
65
- BEGIN
66
- LOOP
67
- -- first try to update the key
68
- UPDATE #{quote_ident(table_name)} SET #{column_definitions.map { |c| "#{c.name} = #{c.input_name}" }.join(',')} WHERE #{example_row.raw_selector.keys.map { |k| "#{quote_ident(k)} = #{quote_ident([k,'input'].join('_'))}" }.join(' AND ') };
69
- IF found THEN
70
- RETURN;
71
- END IF;
72
- -- not there, so try to insert the key
73
- -- if someone else inserts the same key concurrently,
74
- -- we could get a unique-key failure
75
- BEGIN
76
- INSERT INTO #{quote_ident(table_name)}(#{column_definitions.map { |c| c.name }.join(',')}) VALUES (#{column_definitions.map { |c| c.input_name }.join(',')});
77
- RETURN;
78
- EXCEPTION WHEN unique_violation THEN
79
- -- Do nothing, and loop to try the UPDATE again.
80
- END;
81
- END LOOP;
82
- END;
83
- $$
84
- LANGUAGE plpgsql;
85
- EOS
86
- end
87
- end
88
-
89
- # @private
90
- # backwards compatibility - https://github.com/seamusabshere/upsert/issues/2
91
- PGconn = PG_Connection
92
- end
@@ -1,35 +0,0 @@
1
- class Upsert
2
- module PG_Connection
3
- # @private
4
- # activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
5
- class ColumnDefinition
6
- class << self
7
- def all(connection, table_name)
8
- res = connection.exec <<-EOS
9
- SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS sql_type, d.adsrc AS default
10
- FROM pg_attribute a LEFT JOIN pg_attrdef d
11
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
12
- WHERE a.attrelid = '#{connection.quote_ident(table_name.to_s)}'::regclass
13
- AND a.attnum > 0 AND NOT a.attisdropped
14
- ORDER BY a.attnum
15
- EOS
16
- res.map do |row|
17
- new connection, row['name'], row['sql_type'], row['default']
18
- end
19
- end
20
- end
21
-
22
- attr_reader :name
23
- attr_reader :input_name
24
- attr_reader :sql_type
25
- attr_reader :default
26
-
27
- def initialize(connection, raw_name, sql_type, default)
28
- @name = connection.quote_ident raw_name
29
- @input_name = connection.quote_ident "#{raw_name}_input"
30
- @sql_type = sql_type
31
- @default = default
32
- end
33
- end
34
- end
35
- end
@@ -1,39 +0,0 @@
1
- class Upsert
2
- # @private
3
- module SQLite3_Database
4
- def chunk
5
- return if buffer.empty?
6
- row = buffer.shift
7
- %{INSERT OR IGNORE INTO #{quote_ident(table_name)} (#{row.columns_sql}) VALUES (#{row.values_sql});UPDATE #{quote_ident(table_name)} SET #{row.set_sql} WHERE #{row.where_sql}}
8
- end
9
-
10
- def execute(sql)
11
- connection.execute_batch sql
12
- end
13
-
14
- def quote_string(v)
15
- SINGLE_QUOTE + SQLite3::Database.quote(v) + SINGLE_QUOTE
16
- end
17
-
18
- def quote_binary(v)
19
- X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
20
- end
21
-
22
- def quote_time(v)
23
- quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
24
- end
25
-
26
- def quote_ident(k)
27
- DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
28
- end
29
-
30
- def quote_boolean(v)
31
- s = v ? 't' : 'f'
32
- quote_string s
33
- end
34
-
35
- def quote_big_decimal(v)
36
- v.to_f
37
- end
38
- end
39
- end
@@ -1,18 +0,0 @@
1
- shared_examples_for 'supports binary upserts' do
2
- before do
3
- @fakes = []
4
- 10.times do
5
- @fakes << [Faker::Name.name, Faker::Lorem.paragraphs(10).join("\n\n")]
6
- end
7
- end
8
- it "saves binary one by one" do
9
- @fakes.each do |name, biography|
10
- zipped_biography = Zlib::Deflate.deflate biography
11
- upsert = Upsert.new connection, :pets
12
- assert_creates(Pet, [{:name => name, :zipped_biography => zipped_biography}]) do
13
- upsert.row({:name => name}, {:zipped_biography => Upsert.binary(zipped_biography)})
14
- end
15
- Zlib::Inflate.inflate(Pet.find_by_name(name).zipped_biography).must_equal biography
16
- end
17
- end
18
- end
@@ -1,72 +0,0 @@
1
- shared_examples_for 'is just as correct as other ways' do
2
- unless ENV['CORR'] == 'false'
3
-
4
- describe 'compared to native ActiveRecord' do
5
- it "is as correct as than new/set/save" do
6
- assert_same_result lotsa_records do |records|
7
- records.each do |selector, document|
8
- if pet = Pet.where(selector).first
9
- pet.update_attributes document, :without_protection => true
10
- else
11
- pet = Pet.new
12
- selector.each do |k, v|
13
- pet.send "#{k}=", v
14
- end
15
- document.each do |k, v|
16
- pet.send "#{k}=", v
17
- end
18
- pet.save!
19
- end
20
- end
21
- end
22
- end
23
- it "is as correct as than find_or_create + update_attributes" do
24
- assert_same_result lotsa_records do |records|
25
- dynamic_method = nil
26
- records.each do |selector, document|
27
- dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
28
- pet = Pet.send(dynamic_method, *selector.values)
29
- pet.update_attributes document, :without_protection => true
30
- end
31
- end
32
- end
33
- it "is as correct as than create + rescue/find/update" do
34
- assert_same_result lotsa_records do |records|
35
- dynamic_method = nil
36
- records.each do |selector, document|
37
- dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
38
- begin
39
- Pet.create selector.merge(document), :without_protection => true
40
- rescue
41
- pet = Pet.send(dynamic_method, *selector.values)
42
- pet.update_attributes document, :without_protection => true
43
- end
44
- end
45
- end
46
- end
47
- end
48
- describe 'compared to activerecord-import' do
49
- it "is as correct as faking upserts with activerecord-import" do
50
- unless Pet.connection.respond_to?(:sql_for_on_duplicate_key_update)
51
- flunk "#{Pet.connection} does not support activerecord-import's :on_duplicate_key_update"
52
- end
53
- assert_same_result lotsa_records do |records|
54
- columns = nil
55
- all_values = []
56
- records.each do |selector, document|
57
- columns ||= (selector.keys + document.keys).uniq
58
- all_values << columns.map do |k|
59
- if document.has_key?(k)
60
- # prefer the document so that you can change rows
61
- document[k]
62
- else
63
- selector[k]
64
- end
65
- end
66
- end
67
- Pet.import columns, all_values, :timestamps => false, :on_duplicate_key_update => columns
68
- end
69
- end
70
- end
71
- end
72
- end
@@ -1,94 +0,0 @@
1
- shared_examples_for 'is a database with an upsert trick' do
2
- describe :row do
3
- it "works for a single row (base case)" do
4
- upsert = Upsert.new connection, :pets
5
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
6
- upsert.row({:name => 'Jerry'}, {:gender => 'male'})
7
- end
8
- end
9
- it "works for complex selectors" do
10
- upsert = Upsert.new connection, :pets
11
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male', :tag_number => 4}]) do
12
- upsert.row({:name => 'Jerry', :gender => 'male'}, {:tag_number => 1})
13
- upsert.row({:name => 'Jerry', :gender => 'male'}, {:tag_number => 4})
14
- end
15
- end
16
- it "doesn't nullify columns that are not included in the selector or document" do
17
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male', :tag_number => 4}]) do
18
- one = Upsert.new connection, :pets
19
- one.row({:name => 'Jerry'}, {:gender => 'male'})
20
- two = Upsert.new connection, :pets
21
- two.row({:name => 'Jerry'}, {:tag_number => 4})
22
- end
23
- end
24
- it "works for a single row (not changing anything)" do
25
- upsert = Upsert.new connection, :pets
26
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
27
- upsert.row({:name => 'Jerry'}, {:gender => 'male'})
28
- upsert.row({:name => 'Jerry'}, {:gender => 'male'})
29
- end
30
- end
31
- it "works for a single row (changing something)" do
32
- upsert = Upsert.new connection, :pets
33
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'neutered'}]) do
34
- upsert.row({:name => 'Jerry'}, {:gender => 'male'})
35
- upsert.row({:name => 'Jerry'}, {:gender => 'neutered'})
36
- end
37
- Pet.where(:gender => 'male').count.must_equal 0
38
- end
39
- it "works for a single row with implicit nulls" do
40
- upsert = Upsert.new connection, :pets
41
- assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
42
- upsert.row({:name => 'Inky'}, {})
43
- upsert.row({:name => 'Inky'}, {})
44
- end
45
- end
46
- it "works for a single row with empty document" do
47
- upsert = Upsert.new connection, :pets
48
- assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
49
- upsert.row(:name => 'Inky')
50
- upsert.row(:name => 'Inky')
51
- end
52
- end
53
- it "works for a single row with explicit nulls" do
54
- upsert = Upsert.new connection, :pets
55
- assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
56
- upsert.row({:name => 'Inky'}, {:gender => nil})
57
- upsert.row({:name => 'Inky'}, {:gender => nil})
58
- end
59
- end
60
- it "works with ids" do
61
- jerry = Pet.create :name => 'Jerry', :lovability => 1.0
62
- upsert = Upsert.new connection, :pets
63
- assert_creates(Pet, [{:name => 'Jerry', :lovability => 2.0}]) do
64
- upsert.row({:id => jerry.id}, :lovability => 2.0)
65
- end
66
- end
67
- end
68
- describe :batch do
69
- it "works for multiple rows (base case)" do
70
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
71
- Upsert.batch(connection, :pets) do |upsert|
72
- upsert.row({:name => 'Jerry'}, :gender => 'male')
73
- end
74
- end
75
- end
76
- it "works for multiple rows (not changing anything)" do
77
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
78
- Upsert.batch(connection, :pets) do |upsert|
79
- upsert.row({:name => 'Jerry'}, :gender => 'male')
80
- upsert.row({:name => 'Jerry'}, :gender => 'male')
81
- end
82
- end
83
- end
84
- it "works for multiple rows (changing something)" do
85
- assert_creates(Pet, [{:name => 'Jerry', :gender => 'neutered'}]) do
86
- Upsert.batch(connection, :pets) do |upsert|
87
- upsert.row({:name => 'Jerry'}, :gender => 'male')
88
- upsert.row({:name => 'Jerry'}, :gender => 'neutered')
89
- end
90
- end
91
- Pet.where(:gender => 'male').count.must_equal 0
92
- end
93
- end
94
- end