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
@@ -0,0 +1,13 @@
1
+ class Upsert
2
+ class Buffer
3
+ # @private
4
+ class SQLite3_Database < Buffer
5
+ def ready
6
+ return if rows.empty?
7
+ row = rows.shift
8
+ c = parent.connection
9
+ c.execute %{INSERT OR IGNORE INTO #{parent.quoted_table_name} (#{row.columns_sql}) VALUES (#{row.values_sql}); UPDATE #{parent.quoted_table_name} SET #{row.set_sql} WHERE #{row.where_sql}}
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ require 'upsert/connection/mysql2_client'
2
+ require 'upsert/connection/pg_connection'
3
+ require 'upsert/connection/sqlite3_database'
4
+
5
+ class Upsert
6
+ # @private
7
+ class Connection
8
+ attr_reader :parent
9
+ attr_reader :raw_connection
10
+
11
+ def initialize(parent, raw_connection)
12
+ @parent = parent
13
+ @raw_connection = raw_connection
14
+ end
15
+
16
+ def quote_value(v)
17
+ case v
18
+ when NilClass
19
+ NULL_WORD
20
+ when Upsert::Binary
21
+ quote_binary v # must be defined by base
22
+ when String
23
+ quote_string v # must be defined by base
24
+ when TrueClass, FalseClass
25
+ quote_boolean v
26
+ when BigDecimal
27
+ quote_big_decimal v
28
+ when Numeric
29
+ v
30
+ when Symbol
31
+ quote_string v.to_s
32
+ when Time, DateTime
33
+ quote_time v # must be defined by base
34
+ when Date
35
+ quote_string v.strftime(ISO8601_DATE)
36
+ else
37
+ raise "not sure how to quote #{v.class}: #{v.inspect}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,53 @@
1
+ class Upsert
2
+ class Connection
3
+ # @private
4
+ class Mysql2_Client < Connection
5
+ def execute(sql)
6
+ Upsert.logger.debug { %{[upsert] #{sql}} }
7
+ raw_connection.query sql
8
+ end
9
+
10
+ def quote_boolean(v)
11
+ v ? 'TRUE' : 'FALSE'
12
+ end
13
+
14
+ def quote_string(v)
15
+ SINGLE_QUOTE + raw_connection.escape(v) + SINGLE_QUOTE
16
+ end
17
+
18
+ # This doubles the size of the representation.
19
+ def quote_binary(v)
20
+ X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
21
+ end
22
+
23
+ # put raw binary straight into sql
24
+ # might work if we could get the encoding issues fixed when joining together the values for the sql
25
+ # alias_method :quote_binary, :quote_string
26
+
27
+ def quote_time(v)
28
+ quote_string v.strftime(ISO8601_DATETIME)
29
+ end
30
+
31
+ def quote_ident(k)
32
+ BACKTICK + raw_connection.escape(k.to_s) + BACKTICK
33
+ end
34
+
35
+ def quote_big_decimal(v)
36
+ v.to_s('F')
37
+ end
38
+
39
+ def database_variable_get(k)
40
+ sql = "SHOW VARIABLES LIKE '#{k}'"
41
+ row = execute(sql).first
42
+ case row
43
+ when Array
44
+ row[1]
45
+ when Hash
46
+ row['Value']
47
+ else
48
+ raise "Don't know what to do if connection.query returns a #{row.class}"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,39 @@
1
+ class Upsert
2
+ class Connection
3
+ # @private
4
+ class PG_Connection < Connection
5
+ def execute(sql)
6
+ Upsert.logger.debug { %{[upsert] #{sql}} }
7
+ raw_connection.exec sql
8
+ end
9
+
10
+ def quote_string(v)
11
+ SINGLE_QUOTE + raw_connection.escape_string(v) + SINGLE_QUOTE
12
+ end
13
+
14
+ def quote_binary(v)
15
+ E_AND_SINGLE_QUOTE + raw_connection.escape_bytea(v) + SINGLE_QUOTE
16
+ end
17
+
18
+ def quote_time(v)
19
+ quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
20
+ end
21
+
22
+ def quote_big_decimal(v)
23
+ v.to_s('F')
24
+ end
25
+
26
+ def quote_boolean(v)
27
+ v ? 'TRUE' : 'FALSE'
28
+ end
29
+
30
+ def quote_ident(k)
31
+ raw_connection.quote_ident k.to_s
32
+ end
33
+ end
34
+
35
+ # @private
36
+ # backwards compatibility - https://github.com/seamusabshere/upsert/issues/2
37
+ PGconn = PG_Connection
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ class Upsert
2
+ class Connection
3
+ # @private
4
+ class SQLite3_Database < Connection
5
+ def execute(sql)
6
+ Upsert.logger.debug { %{[upsert] #{sql}} }
7
+ raw_connection.execute_batch sql
8
+ end
9
+
10
+ def quote_string(v)
11
+ SINGLE_QUOTE + SQLite3::Database.quote(v) + SINGLE_QUOTE
12
+ end
13
+
14
+ def quote_binary(v)
15
+ X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
16
+ end
17
+
18
+ def quote_time(v)
19
+ quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
20
+ end
21
+
22
+ def quote_ident(k)
23
+ DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
24
+ end
25
+
26
+ def quote_boolean(v)
27
+ s = v ? 't' : 'f'
28
+ quote_string s
29
+ end
30
+
31
+ def quote_big_decimal(v)
32
+ v.to_f
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/upsert/row.rb CHANGED
@@ -1,20 +1,19 @@
1
1
  class Upsert
2
2
  # @private
3
3
  class Row
4
- attr_reader :parent
5
- attr_reader :raw_selector
4
+ Cell = Struct.new(:quoted_key, :quoted_value)
5
+
6
6
  attr_reader :selector
7
7
  attr_reader :document
8
8
 
9
9
  def initialize(parent, raw_selector, raw_document)
10
- @parent = parent
11
- @raw_selector = raw_selector
10
+ c = parent.connection
12
11
  @selector = raw_selector.inject({}) do |memo, (k, v)|
13
- memo[parent.quote_ident(k)] = parent.quote_value(v)
12
+ memo[k.to_s] = Cell.new(c.quote_ident(k), c.quote_value(v))
14
13
  memo
15
14
  end
16
15
  @document = raw_document.inject({}) do |memo, (k, v)|
17
- memo[parent.quote_ident(k)] = parent.quote_value(v)
16
+ memo[k.to_s] = Cell.new(c.quote_ident(k), c.quote_value(v))
18
17
  memo
19
18
  end
20
19
  end
@@ -24,41 +23,46 @@ class Upsert
24
23
  end
25
24
 
26
25
  def values_sql_bytesize
27
- @values_sql_bytesize ||= pairs.inject(0) { |sum, (_, v)| sum + v.to_s.bytesize } + columns.length - 1
26
+ @values_sql_bytesize ||= quoted_pairs.inject(0) { |sum, (_, v)| sum + v.to_s.bytesize } + columns.length - 1
28
27
  end
29
28
 
30
29
  def values_sql
31
- pairs.map { |_, v| v }.join(',')
30
+ quoted_pairs.map { |_, v| v }.join(',')
32
31
  end
33
32
 
34
33
  def columns_sql
35
- pairs.map { |k, _| k }.join(',')
34
+ quoted_pairs.map { |k, _| k }.join(',')
36
35
  end
37
36
 
38
37
  def where_sql
39
- selector.map { |k, v| [k, v].join('=') }.join(' AND ')
38
+ selector.map { |_, cell| [cell.quoted_key, cell.quoted_value].join('=') }.join(' AND ')
40
39
  end
41
40
 
42
41
  def set_sql
43
- pairs.map { |k, v| [k, v].join('=') }.join(',')
42
+ quoted_pairs.map { |k, v| [k, v].join('=') }.join(',')
44
43
  end
45
44
 
46
- def pairs
47
- @pairs ||= columns.map do |k|
48
- v = if document.has_key?(k)
49
- # prefer the document so that you can change rows
50
- document[k]
51
- else
52
- selector[k]
53
- end
54
- [ k, v ]
45
+ def quoted_value(k)
46
+ if c = cell(k)
47
+ c.quoted_value
55
48
  end
56
49
  end
57
50
 
58
- def to_hash
59
- @to_hash ||= pairs.inject({}) do |memo, (k, v)|
60
- memo[k] = v
61
- memo
51
+ def quoted_pairs
52
+ @quoted_pairs ||= columns.map do |k|
53
+ c = cell k
54
+ [ c.quoted_key, c.quoted_value ]
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def cell(k)
61
+ if document.has_key?(k)
62
+ # prefer the document so that you can change rows
63
+ document[k]
64
+ else
65
+ selector[k]
62
66
  end
63
67
  end
64
68
  end
@@ -1,3 +1,3 @@
1
1
  class Upsert
2
- VERSION = "0.3.4"
2
+ VERSION = "0.4.0"
3
3
  end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ require 'upsert/active_record_upsert'
4
+
5
+ describe Upsert do
6
+ describe 'the optional active_record extension' do
7
+ describe :upsert do
8
+ it "is easy to use" do
9
+ assert_creates(Pet,[{:name => 'Jerry', :good => true}]) do
10
+ Pet.upsert({:name => 'Jerry'}, :good => false)
11
+ Pet.upsert({:name => 'Jerry'}, :good => true)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ describe Upsert do
3
+ describe "supports binary upserts" do
4
+ before do
5
+ @fakes = []
6
+ 10.times do
7
+ @fakes << [Faker::Name.name, Faker::Lorem.paragraphs(10).join("\n\n")]
8
+ end
9
+ end
10
+ it "saves binary one by one" do
11
+ @fakes.each do |name, biography|
12
+ zipped_biography = Zlib::Deflate.deflate biography
13
+ upsert = Upsert.new $conn, :pets
14
+ assert_creates(Pet, [{:name => name, :zipped_biography => zipped_biography}]) do
15
+ upsert.row({:name => name}, {:zipped_biography => Upsert.binary(zipped_biography)})
16
+ end
17
+ Zlib::Inflate.inflate(Pet.find_by_name(name).zipped_biography).should == biography
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+ describe Upsert do
3
+ describe "is just as correct as other ways" do
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
+
49
+ if ENV['ADAPTER'] == 'mysql2'
50
+ describe 'compared to activerecord-import' do
51
+ it "is as correct as faking upserts with activerecord-import" do
52
+ assert_same_result lotsa_records do |records|
53
+ columns = nil
54
+ all_values = []
55
+ records.each do |selector, document|
56
+ columns ||= (selector.keys + document.keys).uniq
57
+ all_values << columns.map do |k|
58
+ if document.has_key?(k)
59
+ # prefer the document so that you can change rows
60
+ document[k]
61
+ else
62
+ selector[k]
63
+ end
64
+ end
65
+ end
66
+ Pet.import columns, all_values, :timestamps => false, :on_duplicate_key_update => columns
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ end
73
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'stringio'
3
+ describe Upsert do
4
+ if ENV['ADAPTER'] == 'postgresql'
5
+ describe 'PostgreSQL database functions' do
6
+ it "re-uses merge functions across connections" do
7
+ begin
8
+ io = StringIO.new
9
+ old_logger = Upsert.logger
10
+ Upsert.logger = Logger.new io, Logger::INFO
11
+
12
+ # clear
13
+ Upsert.new(PGconn.new(:dbname => 'upsert_test'), :pets).buffer.clear_database_functions
14
+
15
+ # create
16
+ Upsert.new(PGconn.new(:dbname => 'upsert_test'), :pets).row :name => 'hello'
17
+
18
+ # clear
19
+ Upsert.new(PGconn.new(:dbname => 'upsert_test'), :pets).buffer.clear_database_functions
20
+
21
+ # create (#2)
22
+ Upsert.new(PGconn.new(:dbname => 'upsert_test'), :pets).row :name => 'hello'
23
+
24
+ # no create!
25
+ Upsert.new(PGconn.new(:dbname => 'upsert_test'), :pets).row :name => 'hello'
26
+
27
+ io.rewind
28
+ hits = io.read.split("\n").grep(/Creating or replacing/)
29
+ hits.length.should == 2
30
+ ensure
31
+ Upsert.logger = old_logger
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+ describe Upsert do
3
+ describe "is a database with an upsert trick" do
4
+ describe :row do
5
+ it "works for a single row (base case)" do
6
+ upsert = Upsert.new $conn, :pets
7
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
8
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
9
+ end
10
+ end
11
+ it "works for complex selectors" do
12
+ upsert = Upsert.new $conn, :pets
13
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male', :tag_number => 4}]) do
14
+ upsert.row({:name => 'Jerry', :gender => 'male'}, {:tag_number => 1})
15
+ upsert.row({:name => 'Jerry', :gender => 'male'}, {:tag_number => 4})
16
+ end
17
+ end
18
+ it "doesn't nullify columns that are not included in the selector or document" do
19
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male', :tag_number => 4}]) do
20
+ one = Upsert.new $conn, :pets
21
+ one.row({:name => 'Jerry'}, {:gender => 'male'})
22
+ two = Upsert.new $conn, :pets
23
+ two.row({:name => 'Jerry'}, {:tag_number => 4})
24
+ end
25
+ end
26
+ it "works for a single row (not changing anything)" do
27
+ upsert = Upsert.new $conn, :pets
28
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
29
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
30
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
31
+ end
32
+ end
33
+ it "works for a single row (changing something)" do
34
+ upsert = Upsert.new $conn, :pets
35
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'neutered'}]) do
36
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
37
+ upsert.row({:name => 'Jerry'}, {:gender => 'neutered'})
38
+ end
39
+ Pet.where(:gender => 'male').count.should == 0
40
+ end
41
+ it "works for a single row with implicit nulls" do
42
+ upsert = Upsert.new $conn, :pets
43
+ assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
44
+ upsert.row({:name => 'Inky'}, {})
45
+ upsert.row({:name => 'Inky'}, {})
46
+ end
47
+ end
48
+ it "works for a single row with empty document" do
49
+ upsert = Upsert.new $conn, :pets
50
+ assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
51
+ upsert.row(:name => 'Inky')
52
+ upsert.row(:name => 'Inky')
53
+ end
54
+ end
55
+ it "works for a single row with explicit nulls" do
56
+ upsert = Upsert.new $conn, :pets
57
+ assert_creates(Pet, [{:name => 'Inky', :gender => nil}]) do
58
+ upsert.row({:name => 'Inky'}, {:gender => nil})
59
+ upsert.row({:name => 'Inky'}, {:gender => nil})
60
+ end
61
+ end
62
+ it "works with ids" do
63
+ jerry = Pet.create :name => 'Jerry', :lovability => 1.0
64
+ upsert = Upsert.new $conn, :pets
65
+ assert_creates(Pet, [{:name => 'Jerry', :lovability => 2.0}]) do
66
+ upsert.row({:id => jerry.id}, :lovability => 2.0)
67
+ end
68
+ end
69
+ end
70
+ describe :batch do
71
+ it "works for multiple rows (base case)" do
72
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
73
+ Upsert.batch($conn, :pets) do |upsert|
74
+ upsert.row({:name => 'Jerry'}, :gender => 'male')
75
+ end
76
+ end
77
+ end
78
+ it "works for multiple rows (not changing anything)" do
79
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
80
+ Upsert.batch($conn, :pets) do |upsert|
81
+ upsert.row({:name => 'Jerry'}, :gender => 'male')
82
+ upsert.row({:name => 'Jerry'}, :gender => 'male')
83
+ end
84
+ end
85
+ end
86
+ it "works for multiple rows (changing something)" do
87
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'neutered'}]) do
88
+ Upsert.batch($conn, :pets) do |upsert|
89
+ upsert.row({:name => 'Jerry'}, :gender => 'male')
90
+ upsert.row({:name => 'Jerry'}, :gender => 'neutered')
91
+ end
92
+ end
93
+ Pet.where(:gender => 'male').count.should == 0
94
+ end
95
+ end
96
+ end
97
+ end