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