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
@@ -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