upsert 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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