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,42 @@
1
+ require 'upsert/merge_function/mysql'
2
+
3
+ class Upsert
4
+ class MergeFunction
5
+ # @private
6
+ class Java_ComMysqlJdbc_JDBC4Connection < MergeFunction
7
+ include Mysql
8
+
9
+ def sql
10
+ @sql ||= begin
11
+ bind_params = Array.new(selector_keys.length + setter_keys.length, '?')
12
+ %{CALL #{name}(#{bind_params.join(', ')})}
13
+ end
14
+ end
15
+
16
+ def execute(row)
17
+ first_try = true
18
+ bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
19
+ bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
20
+ begin
21
+ connection.execute sql, (bind_selector_values + bind_setter_values)
22
+ rescue com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException => e
23
+ if e.message =~ /PROCEDURE.*does not exist/i
24
+ if first_try
25
+ Upsert.logger.info %{[upsert] Function #{name.inspect} went missing, trying to recreate}
26
+ first_try = false
27
+ create!
28
+ retry
29
+ else
30
+ Upsert.logger.info %{[upsert] Failed to create function #{name.inspect} for some reason}
31
+ raise e
32
+ end
33
+ else
34
+ raise e
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ require 'upsert/merge_function/postgresql'
2
+
3
+ class Upsert
4
+ class MergeFunction
5
+ # @private
6
+ class Java_OrgPostgresqlJdbc4_Jdbc4Connection < MergeFunction
7
+ include Postgresql
8
+
9
+ def execute(row)
10
+ first_try = true
11
+ bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
12
+ bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
13
+ begin
14
+ connection.execute sql, (bind_selector_values + bind_setter_values)
15
+ rescue org.postgresql.util.PSQLException => pg_error
16
+ if pg_error.message =~ /function #{name}.* does not exist/i
17
+ if first_try
18
+ Upsert.logger.info %{[upsert] Function #{name.inspect} went missing, trying to recreate}
19
+ first_try = false
20
+ create!
21
+ retry
22
+ else
23
+ Upsert.logger.info %{[upsert] Failed to create function #{name.inspect} for some reason}
24
+ raise pg_error
25
+ end
26
+ else
27
+ raise pg_error
28
+ end
29
+ end
30
+ end
31
+
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ require 'upsert/merge_function/sqlite3'
2
+
3
+ class Upsert
4
+ class MergeFunction
5
+ # @private
6
+ class Java_OrgSqliteConn < MergeFunction
7
+ include Sqlite3
8
+ end
9
+ end
10
+ end
@@ -1,18 +1,14 @@
1
- require 'digest/md5'
1
+ require 'upsert/merge_function/mysql'
2
2
 
3
3
  class Upsert
4
4
  class MergeFunction
5
5
  # @private
6
6
  class Mysql2_Client < MergeFunction
7
- MAX_NAME_LENGTH = 63
7
+ include Mysql
8
8
 
9
- class << self
10
- # http://stackoverflow.com/questions/733349/list-of-stored-procedures-functions-mysql-command-line
11
- def clear(connection)
12
- connection.execute("SHOW PROCEDURE STATUS WHERE Db = DATABASE() AND Name LIKE 'upsert_%'").map { |row| row['Name'] }.each do |name|
13
- connection.execute "DROP PROCEDURE IF EXISTS #{connection.quote_ident(name)}"
14
- end
15
- end
9
+ def sql(row)
10
+ quoted_params = (row.selector.values + row.setter.values).map { |v| connection.quote_value v }
11
+ %{CALL #{name}(#{quoted_params.join(', ')})}
16
12
  end
17
13
 
18
14
  def execute(row)
@@ -35,55 +31,6 @@ class Upsert
35
31
  end
36
32
  end
37
33
  end
38
-
39
- def sql(row)
40
- quoted_params = (row.selector.values + row.setter.values).map(&:quoted_value)
41
- %{CALL #{name}(#{quoted_params.join(', ')})}
42
- end
43
-
44
- # http://stackoverflow.com/questions/11371479/how-to-translate-postgresql-merge-db-aka-upsert-function-into-mysql/
45
- def create!
46
- Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector_keys.map(&:inspect).join(', ')} and setter #{setter_keys.map(&:inspect).join(', ')}"
47
- selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
48
- setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
49
- quoted_name = connection.quote_ident name
50
- connection.execute "DROP PROCEDURE IF EXISTS #{quoted_name}"
51
- connection.execute(%{
52
- CREATE PROCEDURE #{quoted_name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')})
53
- BEGIN
54
- DECLARE done BOOLEAN;
55
- REPEAT
56
- BEGIN
57
- -- If there is a unique key constraint error then
58
- -- someone made a concurrent insert. Reset the sentinel
59
- -- and try again.
60
- DECLARE ER_DUP_UNIQUE CONDITION FOR 23000;
61
- DECLARE ER_INTEG CONDITION FOR 1062;
62
- DECLARE CONTINUE HANDLER FOR ER_DUP_UNIQUE BEGIN
63
- SET done = FALSE;
64
- END;
65
-
66
- DECLARE CONTINUE HANDLER FOR ER_INTEG BEGIN
67
- SET done = TRUE;
68
- END;
69
-
70
- SET done = TRUE;
71
- SELECT COUNT(*) INTO @count FROM #{quoted_table_name} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
72
- -- Race condition here. If a concurrent INSERT is made after
73
- -- the SELECT but before the INSERT below we'll get a duplicate
74
- -- key error. But the handler above will take care of that.
75
- IF @count > 0 THEN
76
- -- UPDATE table_name SET b = b_SET WHERE a = a_SEL;
77
- UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
78
- ELSE
79
- -- INSERT INTO table_name (a, b) VALUES (k, data);
80
- INSERT INTO #{quoted_table_name} (#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:quoted_setter_name).join(', ')});
81
- END IF;
82
- END;
83
- UNTIL done END REPEAT;
84
- END
85
- })
86
- end
87
34
  end
88
35
  end
89
36
  end
@@ -1,47 +1,15 @@
1
+ require 'upsert/merge_function/postgresql'
2
+
1
3
  class Upsert
2
4
  class MergeFunction
3
5
  # @private
4
6
  class PG_Connection < MergeFunction
5
- MAX_NAME_LENGTH = 63
6
-
7
- class << self
8
- def clear(connection)
9
- # http://stackoverflow.com/questions/7622908/postgresql-drop-function-without-knowing-the-number-type-of-parameters
10
- connection.execute(%{
11
- CREATE OR REPLACE FUNCTION pg_temp.upsert_delfunc(text)
12
- RETURNS void AS
13
- $BODY$
14
- DECLARE
15
- _sql text;
16
- BEGIN
17
- FOR _sql IN
18
- SELECT 'DROP FUNCTION ' || quote_ident(n.nspname)
19
- || '.' || quote_ident(p.proname)
20
- || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');'
21
- FROM pg_catalog.pg_proc p
22
- LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
23
- WHERE p.proname = $1
24
- AND pg_catalog.pg_function_is_visible(p.oid) -- you may or may not want this
25
- LOOP
26
- EXECUTE _sql;
27
- END LOOP;
28
- END;
29
- $BODY$
30
- LANGUAGE plpgsql;
31
- })
32
- connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE 'upsert_%'}).each do |row|
33
- k = row['proname']
34
- next if k == 'upsert_delfunc'
35
- Upsert.logger.info %{[upsert] Dropping function #{k.inspect}}
36
- connection.execute %{SELECT pg_temp.upsert_delfunc('#{k}')}
37
- end
38
- end
39
- end
7
+ include Postgresql
40
8
 
41
9
  def execute(row)
42
10
  first_try = true
43
- bind_selector_values = row.selector.values.map(&:bind_value)
44
- bind_setter_values = row.setter.values.map(&:bind_value)
11
+ bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
12
+ bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
45
13
  begin
46
14
  connection.execute sql, (bind_selector_values + bind_setter_values)
47
15
  rescue PG::Error => pg_error
@@ -61,6 +29,7 @@ class Upsert
61
29
  end
62
30
  end
63
31
 
32
+ # strangely ? can't be used as a placeholder
64
33
  def sql
65
34
  @sql ||= begin
66
35
  bind_params = []
@@ -68,47 +37,6 @@ class Upsert
68
37
  %{SELECT #{name}(#{bind_params.join(', ')})}
69
38
  end
70
39
  end
71
-
72
- # the "canonical example" from http://www.postgresql.org/docs/9.1/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
73
- # differentiate between selector and setter
74
- def create!
75
- Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector_keys.map(&:inspect).join(', ')} and setter #{setter_keys.map(&:inspect).join(', ')}"
76
- selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
77
- setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
78
- connection.execute(%{
79
- CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
80
- $$
81
- DECLARE
82
- first_try INTEGER := 1;
83
- BEGIN
84
- LOOP
85
- -- first try to update the key
86
- UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')}
87
- WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
88
- IF found THEN
89
- RETURN;
90
- END IF;
91
- -- not there, so try to insert the key
92
- -- if someone else inserts the same key concurrently,
93
- -- we could get a unique-key failure
94
- BEGIN
95
- INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:quoted_setter_name).join(', ')});
96
- RETURN;
97
- EXCEPTION WHEN unique_violation THEN
98
- -- seamusabshere 9/20/12 only retry once
99
- IF (first_try = 1) THEN
100
- first_try := 0;
101
- ELSE
102
- RETURN;
103
- END IF;
104
- -- Do nothing, and loop to try the UPDATE again.
105
- END;
106
- END LOOP;
107
- END;
108
- $$
109
- LANGUAGE plpgsql;
110
- })
111
- end
112
40
  end
113
41
  end
114
42
  end
@@ -1,29 +1,10 @@
1
+ require 'upsert/merge_function/sqlite3'
2
+
1
3
  class Upsert
2
4
  class MergeFunction
3
5
  # @private
4
6
  class SQLite3_Database < MergeFunction
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(&:bind_value)
20
-
21
- insert_or_ignore_sql = %{INSERT OR IGNORE INTO #{quoted_table_name} (#{quoted_setter_names.join(',')}) VALUES (#{Array.new(bind_setter_values.length, '?').join(',')})}
22
- connection.execute insert_or_ignore_sql, bind_setter_values
23
-
24
- update_sql = %{UPDATE #{quoted_table_name} SET #{quoted_setter_names.map { |qk| "#{qk}=?" }.join(',')} WHERE #{quoted_selector_names.map { |qk| "#{qk}=?" }.join(' AND ')}}
25
- connection.execute update_sql, (bind_setter_values + row.selector.values.map(&:bind_value))
26
- end
7
+ include Sqlite3
27
8
  end
28
9
  end
29
10
  end
@@ -0,0 +1,67 @@
1
+ class Upsert
2
+ class MergeFunction
3
+ # @private
4
+ module Mysql
5
+ MAX_NAME_LENGTH = 63
6
+
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ # http://stackoverflow.com/questions/733349/list-of-stored-procedures-functions-mysql-command-line
13
+ def clear(connection)
14
+ connection.execute("SHOW PROCEDURE STATUS WHERE Db = DATABASE() AND Name LIKE 'upsert_%'").map do |row|
15
+ row['Name'] || row['ROUTINE_NAME']
16
+ end.each do |name|
17
+ connection.execute "DROP PROCEDURE IF EXISTS #{connection.quote_ident(name)}"
18
+ end
19
+ end
20
+ end
21
+
22
+ # http://stackoverflow.com/questions/11371479/how-to-translate-postgresql-merge-db-aka-upsert-function-into-mysql/
23
+ def create!
24
+ Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector_keys.map(&:inspect).join(', ')} and setter #{setter_keys.map(&:inspect).join(', ')}"
25
+ selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
26
+ setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
27
+ quoted_name = connection.quote_ident name
28
+ connection.execute "DROP PROCEDURE IF EXISTS #{quoted_name}"
29
+ connection.execute(%{
30
+ CREATE PROCEDURE #{quoted_name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')})
31
+ BEGIN
32
+ DECLARE done BOOLEAN;
33
+ REPEAT
34
+ BEGIN
35
+ -- If there is a unique key constraint error then
36
+ -- someone made a concurrent insert. Reset the sentinel
37
+ -- and try again.
38
+ DECLARE ER_DUP_UNIQUE CONDITION FOR 23000;
39
+ DECLARE ER_INTEG CONDITION FOR 1062;
40
+ DECLARE CONTINUE HANDLER FOR ER_DUP_UNIQUE BEGIN
41
+ SET done = FALSE;
42
+ END;
43
+
44
+ DECLARE CONTINUE HANDLER FOR ER_INTEG BEGIN
45
+ SET done = TRUE;
46
+ END;
47
+
48
+ SET done = TRUE;
49
+ SELECT COUNT(*) INTO @count FROM #{quoted_table_name} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
50
+ -- Race condition here. If a concurrent INSERT is made after
51
+ -- the SELECT but before the INSERT below we'll get a duplicate
52
+ -- key error. But the handler above will take care of that.
53
+ IF @count > 0 THEN
54
+ -- UPDATE table_name SET b = b_SET WHERE a = a_SEL;
55
+ UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
56
+ ELSE
57
+ -- INSERT INTO table_name (a, b) VALUES (k, data);
58
+ INSERT INTO #{quoted_table_name} (#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:to_setter_value).join(', ')});
59
+ END IF;
60
+ END;
61
+ UNTIL done END REPEAT;
62
+ END
63
+ })
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,94 @@
1
+ class Upsert
2
+ class MergeFunction
3
+ # @private
4
+ module Postgresql
5
+ MAX_NAME_LENGTH = 63
6
+
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+ def clear(connection)
13
+ # http://stackoverflow.com/questions/7622908/postgresql-drop-function-without-knowing-the-number-type-of-parameters
14
+ connection.execute(%{
15
+ CREATE OR REPLACE FUNCTION pg_temp.upsert_delfunc(text)
16
+ RETURNS void AS
17
+ $BODY$
18
+ DECLARE
19
+ _sql text;
20
+ BEGIN
21
+ FOR _sql IN
22
+ SELECT 'DROP FUNCTION ' || quote_ident(n.nspname)
23
+ || '.' || quote_ident(p.proname)
24
+ || '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');'
25
+ FROM pg_catalog.pg_proc p
26
+ LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
27
+ WHERE p.proname = $1
28
+ AND pg_catalog.pg_function_is_visible(p.oid) -- you may or may not want this
29
+ LOOP
30
+ EXECUTE _sql;
31
+ END LOOP;
32
+ END;
33
+ $BODY$
34
+ LANGUAGE plpgsql;
35
+ })
36
+ connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE 'upsert_%'}).each do |row|
37
+ k = row['proname']
38
+ next if k == 'upsert_delfunc'
39
+ Upsert.logger.info %{[upsert] Dropping function #{k.inspect}}
40
+ connection.execute %{SELECT pg_temp.upsert_delfunc('#{k}')}
41
+ end
42
+ end
43
+ end
44
+
45
+ def sql
46
+ @sql ||= begin
47
+ bind_params = Array.new(selector_keys.length + setter_keys.length, '?')
48
+ %{SELECT #{name}(#{bind_params.join(', ')})}
49
+ end
50
+ end
51
+
52
+ # the "canonical example" from http://www.postgresql.org/docs/9.1/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
53
+ # differentiate between selector and setter
54
+ def create!
55
+ Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector_keys.map(&:inspect).join(', ')} and setter #{setter_keys.map(&:inspect).join(', ')}"
56
+ selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
57
+ setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
58
+ connection.execute(%{
59
+ CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
60
+ $$
61
+ DECLARE
62
+ first_try INTEGER := 1;
63
+ BEGIN
64
+ LOOP
65
+ -- first try to update the key
66
+ UPDATE #{quoted_table_name} SET #{setter_column_definitions.map(&:to_setter).join(', ')}
67
+ WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
68
+ IF found THEN
69
+ RETURN;
70
+ END IF;
71
+ -- not there, so try to insert the key
72
+ -- if someone else inserts the same key concurrently,
73
+ -- we could get a unique-key failure
74
+ BEGIN
75
+ INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:to_setter_value).join(', ')});
76
+ RETURN;
77
+ EXCEPTION WHEN unique_violation THEN
78
+ -- seamusabshere 9/20/12 only retry once
79
+ IF (first_try = 1) THEN
80
+ first_try := 0;
81
+ ELSE
82
+ RETURN;
83
+ END IF;
84
+ -- Do nothing, and loop to try the UPDATE again.
85
+ END;
86
+ END LOOP;
87
+ END;
88
+ $$
89
+ LANGUAGE plpgsql;
90
+ })
91
+ end
92
+ end
93
+ end
94
+ end