upsert 1.0.2 → 1.1.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 (49) hide show
  1. data/CHANGELOG +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +115 -66
  4. data/Rakefile +16 -5
  5. data/lib/upsert.rb +86 -25
  6. data/lib/upsert/binary.rb +2 -0
  7. data/lib/upsert/column_definition.rb +27 -3
  8. data/lib/upsert/column_definition/mysql.rb +20 -0
  9. data/lib/upsert/column_definition/{PG_Connection.rb → postgresql.rb} +1 -1
  10. data/lib/upsert/connection.rb +20 -22
  11. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +25 -0
  12. data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +14 -0
  13. data/lib/upsert/connection/Java_OrgSqliteConn.rb +17 -0
  14. data/lib/upsert/connection/Mysql2_Client.rb +40 -18
  15. data/lib/upsert/connection/PG_Connection.rb +7 -3
  16. data/lib/upsert/connection/SQLite3_Database.rb +10 -2
  17. data/lib/upsert/connection/jdbc.rb +81 -0
  18. data/lib/upsert/connection/sqlite3.rb +23 -0
  19. data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
  20. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +35 -0
  21. data/lib/upsert/merge_function/Java_OrgSqliteConn.rb +10 -0
  22. data/lib/upsert/merge_function/Mysql2_Client.rb +5 -58
  23. data/lib/upsert/merge_function/PG_Connection.rb +6 -78
  24. data/lib/upsert/merge_function/SQLite3_Database.rb +3 -22
  25. data/lib/upsert/merge_function/mysql.rb +67 -0
  26. data/lib/upsert/merge_function/postgresql.rb +94 -0
  27. data/lib/upsert/merge_function/sqlite3.rb +30 -0
  28. data/lib/upsert/row.rb +3 -6
  29. data/lib/upsert/version.rb +1 -1
  30. data/spec/binary_spec.rb +0 -2
  31. data/spec/correctness_spec.rb +26 -25
  32. data/spec/database_functions_spec.rb +6 -14
  33. data/spec/logger_spec.rb +22 -10
  34. data/spec/precision_spec.rb +1 -1
  35. data/spec/spec_helper.rb +115 -31
  36. data/spec/speed_spec.rb +1 -1
  37. data/spec/timezones_spec.rb +35 -14
  38. data/spec/type_safety_spec.rb +2 -2
  39. data/upsert.gemspec +18 -6
  40. metadata +25 -38
  41. data/lib/upsert/cell.rb +0 -5
  42. data/lib/upsert/cell/Mysql2_Client.rb +0 -16
  43. data/lib/upsert/cell/PG_Connection.rb +0 -28
  44. data/lib/upsert/cell/SQLite3_Database.rb +0 -36
  45. data/lib/upsert/column_definition/Mysql2_Client.rb +0 -24
  46. data/lib/upsert/column_definition/SQLite3_Database.rb +0 -7
  47. data/lib/upsert/row/Mysql2_Client.rb +0 -21
  48. data/lib/upsert/row/PG_Connection.rb +0 -7
  49. data/lib/upsert/row/SQLite3_Database.rb +0 -7
@@ -0,0 +1,30 @@
1
+ class Upsert
2
+ class MergeFunction
3
+ # @private
4
+ module Sqlite3
5
+ attr_reader :quoted_setter_names
6
+ attr_reader :quoted_selector_names
7
+
8
+ def initialize(*)
9
+ super
10
+ @quoted_setter_names = setter_keys.map { |k| connection.quote_ident k }
11
+ @quoted_selector_names = selector_keys.map { |k| connection.quote_ident k }
12
+ end
13
+
14
+ def create!
15
+ # not necessary
16
+ end
17
+
18
+ def execute(row)
19
+ bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
20
+ bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
21
+
22
+ insert_or_ignore_sql = %{INSERT OR IGNORE INTO #{quoted_table_name} (#{quoted_setter_names.join(',')}) VALUES (#{Array.new(bind_setter_values.length, '?').join(',')})}
23
+ connection.execute insert_or_ignore_sql, bind_setter_values
24
+
25
+ update_sql = %{UPDATE #{quoted_table_name} SET #{quoted_setter_names.map { |qk| "#{qk}=?" }.join(',')} WHERE #{quoted_selector_names.map { |qk| "#{qk}=?" }.join(' AND ')}}
26
+ connection.execute update_sql, (bind_setter_values + bind_selector_values)
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/upsert/row.rb CHANGED
@@ -16,17 +16,14 @@ class Upsert
16
16
  attr_reader :setter
17
17
 
18
18
 
19
- def initialize(controller, raw_selector, raw_setter)
20
- connection = controller.connection
21
- cell_class = controller.cell_class
22
-
19
+ def initialize(raw_selector, raw_setter)
23
20
  @selector = raw_selector.inject({}) do |memo, (k, v)|
24
- memo[k.to_s] = cell_class.new(connection, k, v)
21
+ memo[k.to_s] = v
25
22
  memo
26
23
  end
27
24
 
28
25
  @setter = raw_setter.inject({}) do |memo, (k, v)|
29
- memo[k.to_s] = cell_class.new(connection, k, v)
26
+ memo[k.to_s] = v
30
27
  memo
31
28
  end
32
29
 
@@ -1,3 +1,3 @@
1
1
  class Upsert
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.0"
3
3
  end
data/spec/binary_spec.rb CHANGED
@@ -13,9 +13,7 @@ describe Upsert do
13
13
  upsert = Upsert.new $conn, :pets
14
14
  assert_creates(Pet, [{:name => name, :zipped_biography => zipped_biography}]) do
15
15
  upsert.row({:name => name}, {:zipped_biography => Upsert.binary(zipped_biography)})
16
- # binding.pry
17
16
  end
18
-
19
17
  Zlib::Inflate.inflate(Pet.find_by_name(name).zipped_biography).should == biography
20
18
  end
21
19
  end
@@ -47,6 +47,7 @@ describe Upsert do
47
47
  Pet.find_by_name_and_gender('Jerry', 'blue').tag_number.should == 777
48
48
  end
49
49
  end
50
+
50
51
  describe "is just as correct as other ways" do
51
52
  describe 'compared to native ActiveRecord' do
52
53
  it "is as correct as than new/set/save" do
@@ -67,33 +68,33 @@ describe Upsert do
67
68
  end
68
69
  end
69
70
  end
70
- it "is as correct as than find_or_create + update_attributes" do
71
- assert_same_result lotsa_records do |records|
72
- dynamic_method = nil
73
- records.each do |selector, setter|
74
- dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
75
- pet = Pet.send(dynamic_method, *selector.values)
76
- pet.update_attributes setter, :without_protection => true
77
- end
78
- end
79
- end
80
- it "is as correct as than create + rescue/find/update" do
81
- assert_same_result lotsa_records do |records|
82
- dynamic_method = nil
83
- records.each do |selector, setter|
84
- dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
85
- begin
86
- Pet.create selector.merge(setter), :without_protection => true
87
- rescue
88
- pet = Pet.send(dynamic_method, *selector.values)
89
- pet.update_attributes setter, :without_protection => true
90
- end
91
- end
92
- end
93
- end
71
+ # it "is as correct as than find_or_create + update_attributes" do
72
+ # assert_same_result lotsa_records do |records|
73
+ # dynamic_method = nil
74
+ # records.each do |selector, setter|
75
+ # dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
76
+ # pet = Pet.send(dynamic_method, *selector.values)
77
+ # pet.update_attributes setter, :without_protection => true
78
+ # end
79
+ # end
80
+ # end
81
+ # it "is as correct as than create + rescue/find/update" do
82
+ # assert_same_result lotsa_records do |records|
83
+ # dynamic_method = nil
84
+ # records.each do |selector, setter|
85
+ # dynamic_method ||= "find_or_create_by_#{selector.keys.join('_or_')}"
86
+ # begin
87
+ # Pet.create selector.merge(setter), :without_protection => true
88
+ # rescue
89
+ # pet = Pet.send(dynamic_method, *selector.values)
90
+ # pet.update_attributes setter, :without_protection => true
91
+ # end
92
+ # end
93
+ # end
94
+ # end
94
95
  end
95
96
 
96
- if ENV['ADAPTER'] == 'mysql2'
97
+ if ENV['DB'] == 'mysql'
97
98
  describe 'compared to activerecord-import' do
98
99
  it "is as correct as faking upserts with activerecord-import" do
99
100
  assert_same_result lotsa_records do |records|
@@ -1,14 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'stringio'
3
3
  describe Upsert do
4
- def fresh_connection
5
- case ENV['ADAPTER']
6
- when 'postgresql'
7
- PGconn.new $conn_config
8
- when 'mysql2'
9
- Mysql2::Client.new $conn_config
10
- end
11
- end
12
4
  describe 'database functions' do
13
5
  it "re-uses merge functions across connections" do
14
6
  begin
@@ -17,19 +9,19 @@ describe Upsert do
17
9
  Upsert.logger = Logger.new io, Logger::INFO
18
10
 
19
11
  # clear
20
- Upsert.clear_database_functions(fresh_connection)
12
+ Upsert.clear_database_functions($conn_factory.new_connection)
21
13
 
22
14
  # create
23
- Upsert.new(fresh_connection, :pets).row :name => 'hello'
15
+ Upsert.new($conn_factory.new_connection, :pets).row :name => 'hello'
24
16
 
25
17
  # clear
26
- Upsert.clear_database_functions(fresh_connection)
18
+ Upsert.clear_database_functions($conn_factory.new_connection)
27
19
 
28
20
  # create (#2)
29
- Upsert.new(fresh_connection, :pets).row :name => 'hello'
21
+ Upsert.new($conn_factory.new_connection, :pets).row :name => 'hello'
30
22
 
31
23
  # no create!
32
- Upsert.new(fresh_connection, :pets).row :name => 'hello'
24
+ Upsert.new($conn_factory.new_connection, :pets).row :name => 'hello'
33
25
 
34
26
  io.rewind
35
27
  hits = io.read.split("\n").grep(/Creating or replacing/)
@@ -39,4 +31,4 @@ describe Upsert do
39
31
  end
40
32
  end
41
33
  end
42
- end if %w{ postgresql mysql2 }.include?(ENV['ADAPTER'])
34
+ end if %w{ postgresql mysql }.include?(ENV['DB'])
data/spec/logger_spec.rb CHANGED
@@ -7,7 +7,9 @@ describe Upsert do
7
7
  io = StringIO.new
8
8
  Thread.exclusive do
9
9
  Upsert.logger = Logger.new(io)
10
+
10
11
  Upsert.logger.warn "hello"
12
+
11
13
  io.rewind
12
14
  io.read.chomp.should == 'hello'
13
15
  end
@@ -17,21 +19,31 @@ describe Upsert do
17
19
  end
18
20
 
19
21
  it "logs queries" do
20
- require 'sqlite3'
21
- db = SQLite3::Database.open(':memory:')
22
- db.execute_batch "CREATE TABLE cats (name CHARACTER VARYING(255))"
23
22
  begin
24
- io = StringIO.new
25
23
  old_logger = Upsert.logger
26
- Upsert.logger = Logger.new io, Logger::DEBUG
27
- u = Upsert.new(db, :cats)
28
- u.row :name => 'you'
29
- io.rewind
30
- io.read.chomp.should =~ /INSERT OR IGNORE.*you/mi
24
+ io = StringIO.new
25
+ Thread.exclusive do
26
+ Upsert.logger = Logger.new(io)
27
+
28
+ u = Upsert.new($conn, :pets)
29
+ u.row(name: 'Jerry')
30
+
31
+ io.rewind
32
+ log = io.read.chomp
33
+ case u.connection.class.name
34
+ when /sqlite/i
35
+ log.should =~ /insert or ignore/i
36
+ when /mysql/i
37
+ log.should =~ /call upsert_pets_SEL_name/i
38
+ when /p.*g/i
39
+ log.should =~ /select upsert_pets_SEL_name/i
40
+ else
41
+ raise "not sure"
42
+ end
43
+ end
31
44
  ensure
32
45
  Upsert.logger = old_logger
33
46
  end
34
47
  end
35
-
36
48
  end
37
49
  end
@@ -5,7 +5,7 @@ describe Upsert do
5
5
  small = -0.00000000634943
6
6
  upsert = Upsert.new $conn, :pets
7
7
  upsert.row({:name => 'NotJerry'}, :lovability => small)
8
- Pet.first.lovability.should == small
8
+ Pet.first.lovability.should be_within(1e-11).of(small) # ?
9
9
  end
10
10
  end
11
11
  end
data/spec/spec_helper.rb CHANGED
@@ -4,33 +4,83 @@ require 'bundler/setup'
4
4
  require 'pry'
5
5
 
6
6
  require 'active_record'
7
+ ActiveRecord::Base.default_timezone = :utc
8
+
7
9
  require 'active_record_inline_schema'
8
10
  require 'activerecord-import'
9
11
 
10
- ENV['ADAPTER'] ||= 'mysql2'
11
-
12
- case ENV['ADAPTER']
13
- when 'postgresql'
14
- system %{ dropdb upsert_test }
15
- system %{ createdb upsert_test }
16
- ActiveRecord::Base.establish_connection :adapter => 'postgresql', :database => 'upsert_test'
17
- $conn_config = { :dbname => 'upsert_test' }
18
- $conn = PGconn.new $conn_config
19
- when 'mysql2'
20
- system %{ mysql -u root -ppassword -e "DROP DATABASE IF EXISTS upsert_test" }
21
- system %{ mysql -u root -ppassword -e "CREATE DATABASE upsert_test CHARSET utf8" }
22
- ActiveRecord::Base.establish_connection "#{RUBY_PLATFORM == 'java' ? 'mysql' : 'mysql2'}://root:password@127.0.0.1/upsert_test"
23
- $conn_config = { :username => 'root', :password => 'password', :database => 'upsert_test'}
24
- $conn = Mysql2::Client.new $conn_config
25
- when 'sqlite3'
26
- ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
27
- $conn = ActiveRecord::Base.connection.raw_connection
28
- $conn_config = :use_active_record_raw_connection_yo
29
- else
30
- raise "not supported"
12
+ ENV['DB'] ||= 'mysql'
13
+
14
+ class RawConnectionFactory
15
+ DATABASE = 'upsert_test'
16
+ CURRENT_USER = `whoami`.chomp
17
+ PASSWORD = 'password'
18
+
19
+ case ENV['DB']
20
+
21
+ when 'postgresql'
22
+ Kernel.system %{ dropdb upsert_test }
23
+ Kernel.system %{ createdb upsert_test }
24
+ if RUBY_PLATFORM == 'java'
25
+ CONFIG = "jdbc:postgresql://localhost/#{DATABASE}?user=#{CURRENT_USER}"
26
+ require 'jdbc/postgres'
27
+ # http://thesymanual.wordpress.com/2011/02/21/connecting-jruby-to-postgresql-with-jdbc-postgre-api/
28
+ java.sql.DriverManager.register_driver org.postgresql.Driver.new
29
+ def new_connection
30
+ java.sql.DriverManager.get_connection CONFIG
31
+ end
32
+ else
33
+ CONFIG = { :dbname => DATABASE }
34
+ require 'pg'
35
+ def new_connection
36
+ PG::Connection.new CONFIG
37
+ end
38
+ end
39
+ ActiveRecord::Base.establish_connection :adapter => 'postgresql', :database => DATABASE, :username => CURRENT_USER
40
+
41
+ when 'mysql'
42
+ Kernel.system %{ mysql -u root -ppassword -e "DROP DATABASE IF EXISTS #{DATABASE}" }
43
+ Kernel.system %{ mysql -u root -ppassword -e "CREATE DATABASE #{DATABASE} CHARSET utf8" }
44
+ if RUBY_PLATFORM == 'java'
45
+ CONFIG = "jdbc:mysql://127.0.0.1/#{DATABASE}?user=root&password=password"
46
+ require 'jdbc/mysql'
47
+ java.sql.DriverManager.register_driver com.mysql.jdbc.Driver.new
48
+ def new_connection
49
+ java.sql.DriverManager.get_connection CONFIG
50
+ end
51
+ else
52
+ CONFIG = { :username => 'root', :password => PASSWORD, :database => DATABASE}
53
+ require 'mysql2'
54
+ def new_connection
55
+ Mysql2::Client.new CONFIG
56
+ end
57
+ end
58
+ ActiveRecord::Base.establish_connection "#{RUBY_PLATFORM == 'java' ? 'mysql' : 'mysql2'}://root:password@127.0.0.1/#{DATABASE}"
59
+
60
+ when 'sqlite3'
61
+ if RUBY_PLATFORM == 'java'
62
+ def new_connection
63
+ ActiveRecord::Base.connection.raw_connection.connection
64
+ end
65
+ else
66
+ require 'sqlite3'
67
+ def new_connection
68
+ ActiveRecord::Base.connection.raw_connection
69
+ end
70
+ end
71
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
72
+
73
+ else
74
+ raise "not supported"
75
+ end
31
76
  end
32
77
 
78
+ $conn_factory = RawConnectionFactory.new
79
+ $conn = $conn_factory.new_connection
80
+
33
81
  require 'logger'
82
+ require 'fileutils'
83
+ FileUtils.rm_f 'test.log'
34
84
  ActiveRecord::Base.logger = Logger.new('test.log')
35
85
 
36
86
  if ENV['VERBOSE'] == 'true'
@@ -59,6 +109,17 @@ require 'benchmark'
59
109
  require 'faker'
60
110
 
61
111
  module SpecHelper
112
+ def random_time_or_datetime
113
+ time = Time.at(rand * Time.now.to_i)
114
+ if ENV['DB'] == 'mysql'
115
+ time = time.change(:usec => 0)
116
+ end
117
+ if rand > 0.5
118
+ time = time.change(:usec => 0).to_datetime
119
+ end
120
+ time
121
+ end
122
+
62
123
  def lotsa_records
63
124
  @records ||= begin
64
125
  memo = []
@@ -69,7 +130,7 @@ module SpecHelper
69
130
  2000.times do
70
131
  selector = ActiveSupport::OrderedHash.new
71
132
  selector[:name] = if RUBY_VERSION >= '1.9'
72
- names.sample(1).first
133
+ names.sample
73
134
  else
74
135
  names.choice
75
136
  end
@@ -79,7 +140,7 @@ module SpecHelper
79
140
  :spiel => Faker::Lorem.sentences.join,
80
141
  :good => true,
81
142
  :birthday => Time.at(rand * Time.now.to_i).to_date,
82
- :morning_walk_time => Time.at(rand * Time.now.to_i),
143
+ :morning_walk_time => random_time_or_datetime,
83
144
  :home_address => Faker::Lorem.sentences.join,
84
145
  # hard to know how to have AR insert this properly unless Upsert::Binary subclasses String
85
146
  # :zipped_biography => Upsert.binary(Zlib::Deflate.deflate(Faker::Lorem.paragraphs.join, Zlib::BEST_SPEED))
@@ -102,19 +163,42 @@ module SpecHelper
102
163
  end
103
164
  end
104
165
  ref2 = Pet.order(:name).all.map { |pet| pet.attributes.except('id') }
105
- ref2.each_with_index do |ref2a, i|
106
- ref2a.to_yaml.should == ref1[i].to_yaml
107
- end
108
- # ref2.should == ref1
166
+ compare_attribute_sets ref1, ref2
109
167
  end
110
168
 
111
169
  def assert_creates(model, expected_records)
112
- expected_records.each do |conditions|
113
- model.where(conditions).count.should == 0
170
+ expected_records.each do |selector, setter|
171
+ # should i use setter in where?
172
+ model.where(selector).count.should == 0
114
173
  end
115
174
  yield
116
- expected_records.each do |conditions|
117
- model.where(conditions).count.should == 1
175
+ expected_records.each do |selector, setter|
176
+ setter ||= {}
177
+ found = model.where(selector).map { |record| record.attributes.except('id') }
178
+ expected = [ selector.stringify_keys.merge(setter.stringify_keys) ]
179
+ compare_attribute_sets expected, found
180
+ end
181
+ end
182
+
183
+ def compare_attribute_sets(expected, found)
184
+ e = expected.map { |attrs| simplify_attributes attrs }
185
+ f = found.map { |attrs| simplify_attributes attrs }
186
+ f.each_with_index do |fa, i|
187
+ fa.should == e[i]
188
+ end
189
+ end
190
+
191
+ def simplify_attributes(attrs)
192
+ attrs.select do |k, v|
193
+ v.present?
194
+ end.inject({}) do |memo, (k, v)|
195
+ memo[k] = case v
196
+ when Time, DateTime
197
+ v.to_time.to_f
198
+ else
199
+ v
200
+ end
201
+ memo
118
202
  end
119
203
  end
120
204
 
data/spec/speed_spec.rb CHANGED
@@ -46,7 +46,7 @@ describe Upsert do
46
46
  end
47
47
  end
48
48
 
49
- if ENV['ADAPTER'] == 'mysql2'
49
+ if ENV['DB'] == 'mysql'
50
50
  describe 'compared to activerecord-import' do
51
51
  it "is faster than faking upserts with activerecord-import" do
52
52
  assert_faster_than 'faking upserts with activerecord-import', lotsa_records do |records|
@@ -1,28 +1,49 @@
1
1
  require 'spec_helper'
2
2
  describe Upsert do
3
- describe "doesn't mess with timezones" do
4
- before do
5
- @old_default_tz = ActiveRecord::Base.default_timezone
3
+ describe "timezone support" do
4
+ it "takes times in UTC" do
5
+ time = Time.new.utc
6
+ if ENV['DB'] == 'mysql'
7
+ time = time.change(:usec => 0)
8
+ end
9
+ upsert = Upsert.new $conn, :pets
10
+ assert_creates(Pet, [[{:name => 'Jerry'}, {:morning_walk_time => time}]]) do
11
+ upsert.row({:name => 'Jerry'}, {:morning_walk_time => time})
12
+ end
6
13
  end
7
- after do
8
- ActiveRecord::Base.default_timezone = @old_default_tz
14
+
15
+ it "takes times in local" do
16
+ time = Time.new
17
+ if ENV['DB'] == 'mysql'
18
+ time = time.change(:usec => 0)
19
+ end
20
+ upsert = Upsert.new $conn, :pets
21
+ assert_creates(Pet, [[{:name => 'Jerry'}, {:morning_walk_time => time}]]) do
22
+ upsert.row({:name => 'Jerry'}, {:morning_walk_time => time})
23
+ end
9
24
  end
10
-
11
- it "deals fine with UTC" do
12
- ActiveRecord::Base.default_timezone = :utc
13
- time = Time.now.utc
25
+
26
+ it "takes datetimes in UTC" do
27
+ time = DateTime.now.new_offset(Rational(0, 24))
28
+ if ENV['DB'] == 'mysql'
29
+ time = time.change(:usec => 0)
30
+ end
14
31
  upsert = Upsert.new $conn, :pets
15
- assert_creates(Pet, [{:name => 'Jerry', :morning_walk_time => time}]) do
32
+ assert_creates(Pet, [[{:name => 'Jerry'}, {:morning_walk_time => time}]]) do
16
33
  upsert.row({:name => 'Jerry'}, {:morning_walk_time => time})
17
34
  end
18
35
  end
19
- it "won't mess with UTC" do
20
- ActiveRecord::Base.default_timezone = :local
21
- time = Time.now
36
+
37
+ it "takes datetimes in local" do
38
+ time = DateTime.now
39
+ if ENV['DB'] == 'mysql'
40
+ time = time.change(:usec => 0)
41
+ end
22
42
  upsert = Upsert.new $conn, :pets
23
- assert_creates(Pet, [{:name => 'Jerry', :morning_walk_time => time}]) do
43
+ assert_creates(Pet, [[{:name => 'Jerry'}, {:morning_walk_time => time}]]) do
24
44
  upsert.row({:name => 'Jerry'}, {:morning_walk_time => time})
25
45
  end
26
46
  end
47
+
27
48
  end
28
49
  end