upsert 0.3.4 → 0.4.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 (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