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,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