upsert 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/Gemfile +4 -0
- data/README.md +115 -66
- data/Rakefile +16 -5
- data/lib/upsert.rb +86 -25
- data/lib/upsert/binary.rb +2 -0
- data/lib/upsert/column_definition.rb +27 -3
- data/lib/upsert/column_definition/mysql.rb +20 -0
- data/lib/upsert/column_definition/{PG_Connection.rb → postgresql.rb} +1 -1
- data/lib/upsert/connection.rb +20 -22
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +25 -0
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +14 -0
- data/lib/upsert/connection/Java_OrgSqliteConn.rb +17 -0
- data/lib/upsert/connection/Mysql2_Client.rb +40 -18
- data/lib/upsert/connection/PG_Connection.rb +7 -3
- data/lib/upsert/connection/SQLite3_Database.rb +10 -2
- data/lib/upsert/connection/jdbc.rb +81 -0
- data/lib/upsert/connection/sqlite3.rb +23 -0
- data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +35 -0
- data/lib/upsert/merge_function/Java_OrgSqliteConn.rb +10 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +5 -58
- data/lib/upsert/merge_function/PG_Connection.rb +6 -78
- data/lib/upsert/merge_function/SQLite3_Database.rb +3 -22
- data/lib/upsert/merge_function/mysql.rb +67 -0
- data/lib/upsert/merge_function/postgresql.rb +94 -0
- data/lib/upsert/merge_function/sqlite3.rb +30 -0
- data/lib/upsert/row.rb +3 -6
- data/lib/upsert/version.rb +1 -1
- data/spec/binary_spec.rb +0 -2
- data/spec/correctness_spec.rb +26 -25
- data/spec/database_functions_spec.rb +6 -14
- data/spec/logger_spec.rb +22 -10
- data/spec/precision_spec.rb +1 -1
- data/spec/spec_helper.rb +115 -31
- data/spec/speed_spec.rb +1 -1
- data/spec/timezones_spec.rb +35 -14
- data/spec/type_safety_spec.rb +2 -2
- data/upsert.gemspec +18 -6
- metadata +25 -38
- data/lib/upsert/cell.rb +0 -5
- data/lib/upsert/cell/Mysql2_Client.rb +0 -16
- data/lib/upsert/cell/PG_Connection.rb +0 -28
- data/lib/upsert/cell/SQLite3_Database.rb +0 -36
- data/lib/upsert/column_definition/Mysql2_Client.rb +0 -24
- data/lib/upsert/column_definition/SQLite3_Database.rb +0 -7
- data/lib/upsert/row/Mysql2_Client.rb +0 -21
- data/lib/upsert/row/PG_Connection.rb +0 -7
- 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
|
@@ -1,18 +1,14 @@
|
|
1
|
-
require '
|
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
|
-
|
7
|
+
include Mysql
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
44
|
-
bind_setter_values = row.setter.values.map
|
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
|
-
|
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
|