upsert 0.3.4 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +12 -0
- data/README.md +6 -9
- data/Rakefile +9 -14
- data/lib/upsert.rb +40 -71
- data/lib/upsert/buffer.rb +36 -0
- data/lib/upsert/buffer/mysql2_client.rb +67 -0
- data/lib/upsert/buffer/pg_connection.rb +54 -0
- data/lib/upsert/buffer/pg_connection/merge_function.rb +138 -0
- data/lib/upsert/buffer/sqlite3_database.rb +13 -0
- data/lib/upsert/connection.rb +41 -0
- data/lib/upsert/connection/mysql2_client.rb +53 -0
- data/lib/upsert/connection/pg_connection.rb +39 -0
- data/lib/upsert/connection/sqlite3_database.rb +36 -0
- data/lib/upsert/row.rb +28 -24
- data/lib/upsert/version.rb +1 -1
- data/spec/active_record_upsert_spec.rb +16 -0
- data/spec/binary_spec.rb +21 -0
- data/spec/correctness_spec.rb +73 -0
- data/spec/database_functions_spec.rb +36 -0
- data/spec/database_spec.rb +97 -0
- data/spec/logger_spec.rb +37 -0
- data/{test → spec}/misc/get_postgres_reserved_words.rb +0 -0
- data/{test → spec}/misc/mysql_reserved.txt +0 -0
- data/{test → spec}/misc/pg_reserved.txt +0 -0
- data/spec/multibyte_spec.rb +27 -0
- data/spec/precision_spec.rb +11 -0
- data/spec/reserved_words_spec.rb +46 -0
- data/{test/helper.rb → spec/spec_helper.rb} +43 -43
- data/spec/speed_spec.rb +73 -0
- data/spec/threaded_spec.rb +34 -0
- data/spec/timezones_spec.rb +28 -0
- data/upsert.gemspec +6 -2
- metadata +99 -50
- data/lib/upsert/mysql2_client.rb +0 -104
- data/lib/upsert/pg_connection.rb +0 -92
- data/lib/upsert/pg_connection/column_definition.rb +0 -35
- data/lib/upsert/sqlite3_database.rb +0 -39
- data/test/shared/binary.rb +0 -18
- data/test/shared/correctness.rb +0 -72
- data/test/shared/database.rb +0 -94
- data/test/shared/multibyte.rb +0 -37
- data/test/shared/precision.rb +0 -8
- data/test/shared/reserved_words.rb +0 -45
- data/test/shared/speed.rb +0 -72
- data/test/shared/threaded.rb +0 -31
- data/test/shared/timezones.rb +0 -25
- data/test/test_active_record_connection_adapter.rb +0 -36
- data/test/test_active_record_upsert.rb +0 -23
- data/test/test_mysql2.rb +0 -43
- data/test/test_pg.rb +0 -45
- data/test/test_sqlite.rb +0 -47
data/lib/upsert/mysql2_client.rb
DELETED
@@ -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
|
data/lib/upsert/pg_connection.rb
DELETED
@@ -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
|
data/test/shared/binary.rb
DELETED
@@ -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
|
data/test/shared/correctness.rb
DELETED
@@ -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
|
data/test/shared/database.rb
DELETED
@@ -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
|