upsert 0.1.2 → 0.2.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.
- data/.gitignore +1 -0
- data/README.md +14 -10
- data/Rakefile +1 -1
- data/lib/upsert.rb +95 -10
- data/lib/upsert/active_record_upsert.rb +12 -0
- data/lib/upsert/mysql2_client.rb +160 -0
- data/lib/upsert/pg_connection.rb +84 -0
- data/lib/upsert/pg_connection/column_definition.rb +60 -0
- data/lib/upsert/row.rb +8 -8
- data/lib/upsert/sqlite3_database.rb +39 -0
- data/lib/upsert/version.rb +1 -1
- data/test/misc/get_postgres_reserved_words.rb +12 -0
- data/test/misc/mysql_reserved.txt +226 -0
- data/test/misc/pg_reserved.txt +742 -0
- data/test/shared/multibyte.rb +2 -2
- data/test/shared/reserved_words.rb +41 -0
- data/test/test_active_record_upsert.rb +23 -0
- data/test/test_mysql2.rb +2 -0
- data/test/test_pg.rb +2 -0
- data/test/test_sqlite.rb +14 -11
- metadata +17 -8
- data/lib/upsert/buffer.rb +0 -58
- data/lib/upsert/buffer/mysql2_client.rb +0 -164
- data/lib/upsert/buffer/pg_connection.rb +0 -87
- data/lib/upsert/buffer/pg_connection/column_definition.rb +0 -60
- data/lib/upsert/buffer/sqlite3_database.rb +0 -43
- data/lib/upsert/quoter.rb +0 -43
data/test/shared/multibyte.rb
CHANGED
@@ -23,8 +23,8 @@ shared_examples_for "supports multibyte" do
|
|
23
23
|
end
|
24
24
|
it "won't overflow" do
|
25
25
|
upsert = Upsert.new connection, :pets
|
26
|
-
if upsert.
|
27
|
-
max = upsert.
|
26
|
+
if upsert.respond_to?(:max_sql_bytesize)
|
27
|
+
max = upsert.send(:max_sql_bytesize)
|
28
28
|
ticks = max / 3 - 2
|
29
29
|
lambda do
|
30
30
|
loop do
|
@@ -0,0 +1,41 @@
|
|
1
|
+
shared_examples_for "doesn't blow up on reserved words" do
|
2
|
+
# collect and uniq reserved words
|
3
|
+
reserved_words = ['mysql_reserved.txt', 'pg_reserved.txt'].map do |basename|
|
4
|
+
File.expand_path("../../misc/#{basename}", __FILE__)
|
5
|
+
end.map do |path|
|
6
|
+
IO.readlines(path)
|
7
|
+
end.flatten.map(&:chomp).select(&:present?).uniq
|
8
|
+
|
9
|
+
# make lots of AR models, each of which has 10 columns named after these words
|
10
|
+
nasties = []
|
11
|
+
reserved_words.each_slice(10) do |words|
|
12
|
+
eval %{
|
13
|
+
class Nasty#{nasties.length} < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
}
|
16
|
+
nasty = Object.const_get("Nasty#{nasties.length}")
|
17
|
+
nasty.class_eval do
|
18
|
+
self.primary_key = 'fake_primary_key'
|
19
|
+
col :fake_primary_key
|
20
|
+
words.each do |word|
|
21
|
+
col word
|
22
|
+
end
|
23
|
+
end
|
24
|
+
nasties << [ nasty, words ]
|
25
|
+
end
|
26
|
+
nasties.each do |nasty, _|
|
27
|
+
nasty.auto_upgrade!
|
28
|
+
end
|
29
|
+
|
30
|
+
nasties.each do |nasty, words|
|
31
|
+
it "doesn't die on reserved words #{words.join(',')}" do
|
32
|
+
upsert = Upsert.new connection, nasty.table_name
|
33
|
+
random = rand(1e3).to_s
|
34
|
+
selector = { :fake_primary_key => random, words.first => words.first }
|
35
|
+
document = words[1..-1].inject({}) { |memo, word| memo[word] = word; memo }
|
36
|
+
assert_creates nasty, [selector.merge(document)] do
|
37
|
+
upsert.row selector, document
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'mysql2'
|
3
|
+
|
4
|
+
system %{ mysql -u root -ppassword -e "DROP DATABASE IF EXISTS test_upsert; CREATE DATABASE test_upsert CHARSET utf8" }
|
5
|
+
ActiveRecord::Base.establish_connection :adapter => 'mysql2', :username => 'root', :password => 'password', :database => 'test_upsert'
|
6
|
+
|
7
|
+
require 'upsert/active_record_upsert'
|
8
|
+
|
9
|
+
describe Upsert::ActiveRecordUpsert do
|
10
|
+
before do
|
11
|
+
ActiveRecord::Base.connection.drop_table(Pet.table_name) rescue nil
|
12
|
+
Pet.auto_upgrade!
|
13
|
+
end
|
14
|
+
|
15
|
+
describe :upsert do
|
16
|
+
it "is easy to use" do
|
17
|
+
assert_creates(Pet,[{:name => 'Jerry', :good => true}]) do
|
18
|
+
Pet.upsert({:name => 'Jerry'}, :good => false)
|
19
|
+
Pet.upsert({:name => 'Jerry'}, :good => true)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/test/test_mysql2.rb
CHANGED
data/test/test_pg.rb
CHANGED
data/test/test_sqlite.rb
CHANGED
@@ -11,19 +11,20 @@ describe "upserting on sqlite" do
|
|
11
11
|
@opened_connections = []
|
12
12
|
ActiveRecord::Base.connection.drop_table(Pet.table_name) rescue nil
|
13
13
|
Pet.auto_upgrade!
|
14
|
-
@connection = new_connection
|
15
|
-
end
|
16
|
-
after do
|
17
|
-
@opened_connections.each { |c| c.close }
|
18
|
-
end
|
19
|
-
|
20
|
-
def new_connection
|
21
|
-
c = SQLite3::Database.open(File.expand_path('../../tmp/test.sqlite3', __FILE__))
|
22
|
-
@opened_connections << c
|
23
|
-
c
|
14
|
+
# @connection = new_connection
|
24
15
|
end
|
16
|
+
# after do
|
17
|
+
# @opened_connections.each { |c| c.close }
|
18
|
+
# end
|
19
|
+
|
20
|
+
# def new_connection
|
21
|
+
# c = SQLite3::Database.open(File.expand_path('../../tmp/test.sqlite3', __FILE__))
|
22
|
+
# @opened_connections << c
|
23
|
+
# c
|
24
|
+
# end
|
25
25
|
def connection
|
26
|
-
@connection
|
26
|
+
# @connection
|
27
|
+
ActiveRecord::Base.connection
|
27
28
|
end
|
28
29
|
|
29
30
|
it_also 'is a database with an upsert trick'
|
@@ -39,4 +40,6 @@ describe "upserting on sqlite" do
|
|
39
40
|
it_also "doesn't mess with timezones"
|
40
41
|
|
41
42
|
it_also 'supports binary upserts'
|
43
|
+
|
44
|
+
# it_also "doesn't blow up on reserved words"
|
42
45
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: upsert
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-06-
|
12
|
+
date: 2012-06-21 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sqlite3
|
@@ -187,24 +187,28 @@ files:
|
|
187
187
|
- README.md
|
188
188
|
- Rakefile
|
189
189
|
- lib/upsert.rb
|
190
|
+
- lib/upsert/active_record_upsert.rb
|
190
191
|
- lib/upsert/binary.rb
|
191
|
-
- lib/upsert/
|
192
|
-
- lib/upsert/
|
193
|
-
- lib/upsert/
|
194
|
-
- lib/upsert/buffer/pg_connection/column_definition.rb
|
195
|
-
- lib/upsert/buffer/sqlite3_database.rb
|
196
|
-
- lib/upsert/quoter.rb
|
192
|
+
- lib/upsert/mysql2_client.rb
|
193
|
+
- lib/upsert/pg_connection.rb
|
194
|
+
- lib/upsert/pg_connection/column_definition.rb
|
197
195
|
- lib/upsert/row.rb
|
196
|
+
- lib/upsert/sqlite3_database.rb
|
198
197
|
- lib/upsert/version.rb
|
199
198
|
- test/helper.rb
|
199
|
+
- test/misc/get_postgres_reserved_words.rb
|
200
|
+
- test/misc/mysql_reserved.txt
|
201
|
+
- test/misc/pg_reserved.txt
|
200
202
|
- test/shared/binary.rb
|
201
203
|
- test/shared/correctness.rb
|
202
204
|
- test/shared/database.rb
|
203
205
|
- test/shared/multibyte.rb
|
206
|
+
- test/shared/reserved_words.rb
|
204
207
|
- test/shared/speed.rb
|
205
208
|
- test/shared/threaded.rb
|
206
209
|
- test/shared/timezones.rb
|
207
210
|
- test/test_active_record_connection_adapter.rb
|
211
|
+
- test/test_active_record_upsert.rb
|
208
212
|
- test/test_mysql2.rb
|
209
213
|
- test/test_pg.rb
|
210
214
|
- test/test_sqlite.rb
|
@@ -236,14 +240,19 @@ summary: Upsert for MySQL, PostgreSQL, and SQLite. Finally, all those SQL MERGE
|
|
236
240
|
codified.
|
237
241
|
test_files:
|
238
242
|
- test/helper.rb
|
243
|
+
- test/misc/get_postgres_reserved_words.rb
|
244
|
+
- test/misc/mysql_reserved.txt
|
245
|
+
- test/misc/pg_reserved.txt
|
239
246
|
- test/shared/binary.rb
|
240
247
|
- test/shared/correctness.rb
|
241
248
|
- test/shared/database.rb
|
242
249
|
- test/shared/multibyte.rb
|
250
|
+
- test/shared/reserved_words.rb
|
243
251
|
- test/shared/speed.rb
|
244
252
|
- test/shared/threaded.rb
|
245
253
|
- test/shared/timezones.rb
|
246
254
|
- test/test_active_record_connection_adapter.rb
|
255
|
+
- test/test_active_record_upsert.rb
|
247
256
|
- test/test_mysql2.rb
|
248
257
|
- test/test_pg.rb
|
249
258
|
- test/test_sqlite.rb
|
data/lib/upsert/buffer.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
class Upsert
|
2
|
-
# @private
|
3
|
-
class Buffer
|
4
|
-
class << self
|
5
|
-
def for(connection, table_name)
|
6
|
-
if connection.respond_to?(:raw_connection)
|
7
|
-
# deal with ActiveRecord::Base.connection or ActiveRecord::Base.connection_pool.checkout
|
8
|
-
connection = connection.raw_connection
|
9
|
-
end
|
10
|
-
const_get(connection.class.name.gsub(/\W+/, '_')).new connection, table_name
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
SINGLE_QUOTE = %{'}
|
15
|
-
DOUBLE_QUOTE = %{"}
|
16
|
-
BACKTICK = %{`}
|
17
|
-
E_AND_SINGLE_QUOTE = %{E'}
|
18
|
-
X_AND_SINGLE_QUOTE = %{x'}
|
19
|
-
USEC_SPRINTF = '%06d'
|
20
|
-
ISO8601_DATETIME = '%Y-%m-%d %H:%M:%S'
|
21
|
-
|
22
|
-
attr_reader :connection
|
23
|
-
attr_reader :table_name
|
24
|
-
attr_reader :rows
|
25
|
-
|
26
|
-
def initialize(connection, table_name)
|
27
|
-
@connection = connection
|
28
|
-
@table_name = table_name
|
29
|
-
@rows = []
|
30
|
-
end
|
31
|
-
|
32
|
-
def async?
|
33
|
-
!!@async
|
34
|
-
end
|
35
|
-
|
36
|
-
def async!
|
37
|
-
@async = true
|
38
|
-
end
|
39
|
-
|
40
|
-
def sync!
|
41
|
-
@async = false
|
42
|
-
clear
|
43
|
-
end
|
44
|
-
|
45
|
-
def add(selector, document)
|
46
|
-
rows << Row.new(self, selector, document)
|
47
|
-
if sql = chunk
|
48
|
-
execute sql
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def clear
|
53
|
-
while sql = chunk
|
54
|
-
execute sql
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
58
|
-
end
|
@@ -1,164 +0,0 @@
|
|
1
|
-
class Upsert
|
2
|
-
class Buffer
|
3
|
-
# @private
|
4
|
-
class Mysql2_Client < Buffer
|
5
|
-
include Quoter
|
6
|
-
|
7
|
-
def chunk
|
8
|
-
return if rows.empty?
|
9
|
-
all = rows.length
|
10
|
-
take = all
|
11
|
-
while take > 1 and probably_oversize?(take)
|
12
|
-
take -= 1
|
13
|
-
end
|
14
|
-
if async? and take == all
|
15
|
-
return
|
16
|
-
end
|
17
|
-
while take > 1 and oversize?(take)
|
18
|
-
$stderr.puts " Length prediction via sampling failed, shrinking" if ENV['UPSERT_DEBUG'] == 'true'
|
19
|
-
take -= 1
|
20
|
-
end
|
21
|
-
chunk = sql take
|
22
|
-
while take > 1 and chunk.bytesize > max_sql_bytesize
|
23
|
-
$stderr.puts " Supposedly exact bytesize guess failed, shrinking" if ENV['UPSERT_DEBUG'] == 'true'
|
24
|
-
take -= 1
|
25
|
-
chunk = sql take
|
26
|
-
end
|
27
|
-
if chunk.bytesize > max_sql_bytesize
|
28
|
-
raise TooBig
|
29
|
-
end
|
30
|
-
$stderr.puts " Chunk (#{take}/#{chunk.bytesize}) was #{(chunk.bytesize / max_sql_bytesize.to_f * 100).round}% of the max" if ENV['UPSERT_DEBUG'] == 'true'
|
31
|
-
@rows = rows.drop(take)
|
32
|
-
chunk
|
33
|
-
end
|
34
|
-
|
35
|
-
def execute(sql)
|
36
|
-
connection.query sql
|
37
|
-
end
|
38
|
-
|
39
|
-
def probably_oversize?(take)
|
40
|
-
estimate_sql_bytesize(take) > max_sql_bytesize
|
41
|
-
end
|
42
|
-
|
43
|
-
def oversize?(take)
|
44
|
-
sql_bytesize(take) > max_sql_bytesize
|
45
|
-
end
|
46
|
-
|
47
|
-
def columns
|
48
|
-
@columns ||= rows.first.columns
|
49
|
-
end
|
50
|
-
|
51
|
-
def insert_part
|
52
|
-
@insert_part ||= %{INSERT INTO "#{table_name}" (#{quote_idents(columns)}) VALUES }
|
53
|
-
end
|
54
|
-
|
55
|
-
def update_part
|
56
|
-
@update_part ||= begin
|
57
|
-
updaters = columns.map do |k|
|
58
|
-
qk = quote_ident k
|
59
|
-
[ qk, "VALUES(#{qk})" ].join('=')
|
60
|
-
end.join(',')
|
61
|
-
%{ ON DUPLICATE KEY UPDATE #{updaters}}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
# where 2 is the parens
|
66
|
-
def static_sql_bytesize
|
67
|
-
@static_sql_bytesize ||= insert_part.bytesize + update_part.bytesize + 2
|
68
|
-
end
|
69
|
-
|
70
|
-
# where 3 is parens and comma
|
71
|
-
def variable_sql_bytesize(take)
|
72
|
-
rows.first(take).inject(0) { |sum, row| sum + row.values_sql_bytesize + 3 }
|
73
|
-
end
|
74
|
-
|
75
|
-
def estimate_variable_sql_bytesize(take)
|
76
|
-
p = (take / 10.0).ceil
|
77
|
-
10.0 * rows.sample(p).inject(0) { |sum, row| sum + row.values_sql_bytesize + 3 }
|
78
|
-
end
|
79
|
-
|
80
|
-
def sql_bytesize(take)
|
81
|
-
static_sql_bytesize + variable_sql_bytesize(take)
|
82
|
-
end
|
83
|
-
|
84
|
-
def estimate_sql_bytesize(take)
|
85
|
-
static_sql_bytesize + estimate_variable_sql_bytesize(take)
|
86
|
-
end
|
87
|
-
|
88
|
-
def sql(take)
|
89
|
-
all_value_sql = rows.first(take).map { |row| row.values_sql }
|
90
|
-
[ insert_part, '(', all_value_sql.join('),('), ')', update_part ].join
|
91
|
-
end
|
92
|
-
|
93
|
-
# since setting an option like :as => :hash actually persists that option to the client, don't pass any options
|
94
|
-
def max_sql_bytesize
|
95
|
-
@max_sql_bytesize ||= begin
|
96
|
-
case (row = connection.query("SHOW VARIABLES LIKE 'max_allowed_packet'").first)
|
97
|
-
when Array
|
98
|
-
row[1]
|
99
|
-
when Hash
|
100
|
-
row['Value']
|
101
|
-
else
|
102
|
-
raise "Don't know what to do if connection.query returns a #{row.class}"
|
103
|
-
end.to_i
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
def quoted_value_bytesize(v)
|
108
|
-
case v
|
109
|
-
when NilClass
|
110
|
-
4
|
111
|
-
when TrueClass
|
112
|
-
4
|
113
|
-
when FalseClass
|
114
|
-
5
|
115
|
-
when BigDecimal
|
116
|
-
v.to_s('F').bytesize
|
117
|
-
when Upsert::Binary
|
118
|
-
v.bytesize * 2 + 3
|
119
|
-
when Numeric
|
120
|
-
v.to_s.bytesize
|
121
|
-
when String
|
122
|
-
v.bytesize + 2
|
123
|
-
when Symbol
|
124
|
-
v.to_s.bytesize + 2
|
125
|
-
when Time, DateTime
|
126
|
-
24 + 2
|
127
|
-
when Date
|
128
|
-
10 + 2
|
129
|
-
else
|
130
|
-
raise "not sure how to get quoted length of #{v.class}: #{v.inspect}"
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def quote_boolean(v)
|
135
|
-
v ? 'TRUE' : 'FALSE'
|
136
|
-
end
|
137
|
-
|
138
|
-
def quote_string(v)
|
139
|
-
SINGLE_QUOTE + connection.escape(v) + SINGLE_QUOTE
|
140
|
-
end
|
141
|
-
|
142
|
-
# This doubles the size of the representation.
|
143
|
-
def quote_binary(v)
|
144
|
-
X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
|
145
|
-
end
|
146
|
-
|
147
|
-
# put raw binary straight into sql
|
148
|
-
# might work if we could get the encoding issues fixed when joining together the values for the sql
|
149
|
-
# alias_method :quote_binary, :quote_string
|
150
|
-
|
151
|
-
def quote_time(v)
|
152
|
-
quote_string v.strftime(ISO8601_DATETIME)
|
153
|
-
end
|
154
|
-
|
155
|
-
def quote_ident(k)
|
156
|
-
BACKTICK + connection.escape(k.to_s) + BACKTICK
|
157
|
-
end
|
158
|
-
|
159
|
-
def quote_big_decimal(v)
|
160
|
-
v.to_s('F')
|
161
|
-
end
|
162
|
-
end
|
163
|
-
end
|
164
|
-
end
|
@@ -1,87 +0,0 @@
|
|
1
|
-
require 'upsert/buffer/pg_connection/column_definition'
|
2
|
-
|
3
|
-
class Upsert
|
4
|
-
class Buffer
|
5
|
-
# @private
|
6
|
-
class PG_Connection < Buffer
|
7
|
-
include Quoter
|
8
|
-
|
9
|
-
attr_reader :merge_function
|
10
|
-
|
11
|
-
def chunk
|
12
|
-
return if rows.empty?
|
13
|
-
row = rows.shift
|
14
|
-
unless merge_function
|
15
|
-
create_merge_function row
|
16
|
-
end
|
17
|
-
hsh = row.to_hash
|
18
|
-
ordered_args = column_definitions.map do |c|
|
19
|
-
hsh[c.name]
|
20
|
-
end
|
21
|
-
%{SELECT #{merge_function}(#{quote_values(ordered_args)})}
|
22
|
-
end
|
23
|
-
|
24
|
-
def execute(sql)
|
25
|
-
connection.exec sql
|
26
|
-
end
|
27
|
-
|
28
|
-
def quote_string(v)
|
29
|
-
SINGLE_QUOTE + connection.escape_string(v) + SINGLE_QUOTE
|
30
|
-
end
|
31
|
-
|
32
|
-
def quote_binary(v)
|
33
|
-
E_AND_SINGLE_QUOTE + connection.escape_bytea(v) + SINGLE_QUOTE
|
34
|
-
end
|
35
|
-
|
36
|
-
def quote_time(v)
|
37
|
-
quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
|
38
|
-
end
|
39
|
-
|
40
|
-
def quote_big_decimal(v)
|
41
|
-
v.to_s('F')
|
42
|
-
end
|
43
|
-
|
44
|
-
def quote_boolean(v)
|
45
|
-
v ? 'TRUE' : 'FALSE'
|
46
|
-
end
|
47
|
-
|
48
|
-
def quote_ident(k)
|
49
|
-
DOUBLE_QUOTE + connection.quote_ident(k.to_s) + DOUBLE_QUOTE
|
50
|
-
end
|
51
|
-
|
52
|
-
def column_definitions
|
53
|
-
@column_definitions ||= ColumnDefinition.all(connection, table_name)
|
54
|
-
end
|
55
|
-
|
56
|
-
private
|
57
|
-
|
58
|
-
def create_merge_function(example_row)
|
59
|
-
@merge_function = "pg_temp.merge_#{table_name}_#{Kernel.rand(1e11)}"
|
60
|
-
execute <<-EOS
|
61
|
-
CREATE FUNCTION #{merge_function}(#{column_definitions.map { |c| "#{c.name}_input #{c.sql_type} DEFAULT #{c.default || 'NULL'}" }.join(',') }) RETURNS VOID AS
|
62
|
-
$$
|
63
|
-
BEGIN
|
64
|
-
LOOP
|
65
|
-
-- first try to update the key
|
66
|
-
UPDATE #{table_name} SET #{column_definitions.map { |c| "#{c.name} = #{c.name}_input" }.join(',')} WHERE #{example_row.selector.keys.map { |k| "#{k} = #{k}_input" }.join(' AND ') };
|
67
|
-
IF found THEN
|
68
|
-
RETURN;
|
69
|
-
END IF;
|
70
|
-
-- not there, so try to insert the key
|
71
|
-
-- if someone else inserts the same key concurrently,
|
72
|
-
-- we could get a unique-key failure
|
73
|
-
BEGIN
|
74
|
-
INSERT INTO #{table_name}(#{column_definitions.map { |c| c.name }.join(',')}) VALUES (#{column_definitions.map { |c| "#{c.name}_input" }.join(',')});
|
75
|
-
RETURN;
|
76
|
-
EXCEPTION WHEN unique_violation THEN
|
77
|
-
-- Do nothing, and loop to try the UPDATE again.
|
78
|
-
END;
|
79
|
-
END LOOP;
|
80
|
-
END;
|
81
|
-
$$
|
82
|
-
LANGUAGE plpgsql;
|
83
|
-
EOS
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|