upsert 2.9.9-universal-java-11
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.standard.yml +1 -0
- data/.travis.yml +63 -0
- data/.yardopts +2 -0
- data/CHANGELOG +265 -0
- data/Gemfile +16 -0
- data/LICENSE +24 -0
- data/README.md +411 -0
- data/Rakefile +54 -0
- data/lib/upsert.rb +284 -0
- data/lib/upsert/active_record_upsert.rb +12 -0
- data/lib/upsert/binary.rb +8 -0
- data/lib/upsert/column_definition.rb +79 -0
- data/lib/upsert/column_definition/mysql.rb +24 -0
- data/lib/upsert/column_definition/postgresql.rb +66 -0
- data/lib/upsert/column_definition/sqlite3.rb +34 -0
- data/lib/upsert/connection.rb +37 -0
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
- data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
- data/lib/upsert/connection/Mysql2_Client.rb +76 -0
- data/lib/upsert/connection/PG_Connection.rb +35 -0
- data/lib/upsert/connection/SQLite3_Database.rb +28 -0
- data/lib/upsert/connection/jdbc.rb +105 -0
- data/lib/upsert/connection/postgresql.rb +24 -0
- data/lib/upsert/connection/sqlite3.rb +19 -0
- data/lib/upsert/merge_function.rb +73 -0
- data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
- data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
- data/lib/upsert/merge_function/PG_Connection.rb +26 -0
- data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
- data/lib/upsert/merge_function/mysql.rb +66 -0
- data/lib/upsert/merge_function/postgresql.rb +365 -0
- data/lib/upsert/merge_function/sqlite3.rb +43 -0
- data/lib/upsert/row.rb +59 -0
- data/lib/upsert/version.rb +3 -0
- data/spec/active_record_upsert_spec.rb +26 -0
- data/spec/binary_spec.rb +21 -0
- data/spec/correctness_spec.rb +190 -0
- data/spec/database_functions_spec.rb +106 -0
- data/spec/database_spec.rb +121 -0
- data/spec/hstore_spec.rb +249 -0
- data/spec/jruby_spec.rb +9 -0
- data/spec/logger_spec.rb +52 -0
- data/spec/misc/get_postgres_reserved_words.rb +12 -0
- data/spec/misc/mysql_reserved.txt +226 -0
- data/spec/misc/pg_reserved.txt +742 -0
- data/spec/multibyte_spec.rb +27 -0
- data/spec/postgresql_spec.rb +94 -0
- data/spec/precision_spec.rb +11 -0
- data/spec/reserved_words_spec.rb +50 -0
- data/spec/sequel_spec.rb +57 -0
- data/spec/spec_helper.rb +417 -0
- data/spec/speed_spec.rb +44 -0
- data/spec/threaded_spec.rb +57 -0
- data/spec/timezones_spec.rb +58 -0
- data/spec/type_safety_spec.rb +12 -0
- data/travis/install_postgres.sh +18 -0
- data/travis/run_docker_db.sh +20 -0
- data/travis/tune_mysql.sh +7 -0
- data/upsert-java.gemspec +13 -0
- data/upsert.gemspec +11 -0
- data/upsert.gemspec.common +107 -0
- metadata +373 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
class Upsert
|
2
|
+
class Connection
|
3
|
+
# @private
|
4
|
+
module Postgresql
|
5
|
+
def bind_value(v)
|
6
|
+
case v
|
7
|
+
when Array
|
8
|
+
# pg array escaping lifted from https://github.com/tlconnor/activerecord-postgres-array/blob/master/lib/activerecord-postgres-array/array.rb
|
9
|
+
'{' + v.map do |vv|
|
10
|
+
vv = vv.to_s.dup
|
11
|
+
vv.gsub!(/\\/, '\&\&')
|
12
|
+
vv.gsub!(/"/, '\"')
|
13
|
+
%{"#{vv}"}
|
14
|
+
end.join(',') + '}'
|
15
|
+
when Hash
|
16
|
+
# you must require 'pg_hstore' from the 'pg-hstore' gem yourself
|
17
|
+
::PgHstore.dump v, true
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'upsert/version'
|
3
|
+
|
4
|
+
class Upsert
|
5
|
+
# @private
|
6
|
+
class MergeFunction
|
7
|
+
MAX_NAME_LENGTH = 62
|
8
|
+
NAME_PREFIX = "upsert#{Upsert::VERSION.gsub('.', '_')}"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def unique_name(table_name, selector_keys, setter_keys)
|
12
|
+
parts = [
|
13
|
+
NAME_PREFIX,
|
14
|
+
[*table_name].join("_").gsub(/[^\w_]+/, "_"),
|
15
|
+
'SEL',
|
16
|
+
selector_keys.join('_A_').gsub(" ","_"),
|
17
|
+
'SET',
|
18
|
+
setter_keys.join('_A_').gsub(" ","_")
|
19
|
+
].join('_')
|
20
|
+
if parts.length > MAX_NAME_LENGTH
|
21
|
+
# maybe i should md5 instead
|
22
|
+
crc32 = Zlib.crc32(parts).to_s
|
23
|
+
[ parts[0..MAX_NAME_LENGTH-10], crc32 ].join
|
24
|
+
else
|
25
|
+
parts
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :controller
|
31
|
+
attr_reader :selector_keys
|
32
|
+
attr_reader :setter_keys
|
33
|
+
|
34
|
+
def initialize(controller, selector_keys, setter_keys, assume_function_exists)
|
35
|
+
@controller = controller
|
36
|
+
@selector_keys = selector_keys
|
37
|
+
@setter_keys = setter_keys
|
38
|
+
@assume_function_exists = assume_function_exists
|
39
|
+
validate!
|
40
|
+
create! unless @assume_function_exists
|
41
|
+
end
|
42
|
+
|
43
|
+
def name
|
44
|
+
@name ||= self.class.unique_name table_name, selector_keys, setter_keys
|
45
|
+
end
|
46
|
+
|
47
|
+
def connection
|
48
|
+
controller.connection
|
49
|
+
end
|
50
|
+
|
51
|
+
def table_name
|
52
|
+
controller.table_name
|
53
|
+
end
|
54
|
+
|
55
|
+
def quoted_table_name
|
56
|
+
controller.quoted_table_name
|
57
|
+
end
|
58
|
+
|
59
|
+
def column_definitions
|
60
|
+
controller.column_definitions
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def validate!
|
66
|
+
possible = column_definitions.map(&:name)
|
67
|
+
invalid = (setter_keys + selector_keys).uniq - possible
|
68
|
+
if invalid.any?
|
69
|
+
raise ArgumentError, "[Upsert] Invalid column(s): #{invalid.map(&:inspect).join(', ')}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -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,27 @@
|
|
1
|
+
require 'upsert/merge_function/postgresql'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class MergeFunction
|
5
|
+
# @private
|
6
|
+
class Java_OrgPostgresqlJdbc_PgConnection < MergeFunction
|
7
|
+
ERROR_CLASS = org.postgresql.util.PSQLException
|
8
|
+
include Postgresql
|
9
|
+
|
10
|
+
def execute_parameterized(query, args = [])
|
11
|
+
query_args = []
|
12
|
+
query = query.gsub(/\$(\d+)/) do |str|
|
13
|
+
query_args << args[Regexp.last_match[1].to_i - 1]
|
14
|
+
"?"
|
15
|
+
end
|
16
|
+
controller.connection.execute(query, query_args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def unique_index_on_selector?
|
20
|
+
return @unique_index_on_selector if defined?(@unique_index_on_selector)
|
21
|
+
@unique_index_on_selector = unique_index_columns.any? do |row|
|
22
|
+
row["index_columns"].sort == selector_keys.sort
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'upsert/merge_function/mysql'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class MergeFunction
|
5
|
+
# @private
|
6
|
+
class Mysql2_Client < MergeFunction
|
7
|
+
include Mysql
|
8
|
+
|
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(', ')})}
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute(row)
|
15
|
+
first_try = true
|
16
|
+
begin
|
17
|
+
connection.execute sql(row)
|
18
|
+
rescue Mysql2::Error => e
|
19
|
+
if e.message =~ /PROCEDURE.*does not exist/i
|
20
|
+
if first_try
|
21
|
+
Upsert.logger.info %{[upsert] Function #{name.inspect} went missing, trying to recreate}
|
22
|
+
first_try = false
|
23
|
+
create!
|
24
|
+
retry
|
25
|
+
else
|
26
|
+
Upsert.logger.info %{[upsert] Failed to create function #{name.inspect} for some reason}
|
27
|
+
raise e
|
28
|
+
end
|
29
|
+
else
|
30
|
+
raise e
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'upsert/merge_function/postgresql'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class MergeFunction
|
5
|
+
# @private
|
6
|
+
class PG_Connection < MergeFunction
|
7
|
+
ERROR_CLASS = PG::Error
|
8
|
+
include Postgresql
|
9
|
+
|
10
|
+
def execute_parameterized(query, args = [])
|
11
|
+
controller.connection.execute(query, args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def unique_index_on_selector?
|
15
|
+
return @unique_index_on_selector if defined?(@unique_index_on_selector)
|
16
|
+
|
17
|
+
type_map = PG::TypeMapByColumn.new([PG::TextDecoder::Array.new])
|
18
|
+
res = unique_index_columns.tap { |r| r.type_map = type_map }
|
19
|
+
|
20
|
+
@unique_index_on_selector = res.values.any? do |row|
|
21
|
+
row.first.sort == selector_keys.sort
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
class Upsert
|
2
|
+
class MergeFunction
|
3
|
+
# @private
|
4
|
+
module Mysql
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
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 '#{MergeFunction::NAME_PREFIX}%'").map do |row|
|
13
|
+
row['Name'] || row['ROUTINE_NAME']
|
14
|
+
end.each do |name|
|
15
|
+
connection.execute "DROP PROCEDURE IF EXISTS #{connection.quote_ident(name)}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# http://stackoverflow.com/questions/11371479/how-to-translate-postgresql-merge-db-aka-upsert-function-into-mysql/
|
21
|
+
def create!
|
22
|
+
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(', ')}"
|
23
|
+
selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
|
24
|
+
setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
|
25
|
+
update_column_definitions = setter_column_definitions.select { |cd| cd.name !~ CREATED_COL_REGEX }
|
26
|
+
quoted_name = connection.quote_ident name
|
27
|
+
connection.execute "DROP PROCEDURE IF EXISTS #{quoted_name}"
|
28
|
+
connection.execute(%{
|
29
|
+
CREATE PROCEDURE #{quoted_name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')})
|
30
|
+
BEGIN
|
31
|
+
DECLARE done BOOLEAN;
|
32
|
+
REPEAT
|
33
|
+
BEGIN
|
34
|
+
-- If there is a unique key constraint error then
|
35
|
+
-- someone made a concurrent insert. Reset the sentinel
|
36
|
+
-- and try again.
|
37
|
+
DECLARE ER_DUP_UNIQUE CONDITION FOR 23000;
|
38
|
+
DECLARE ER_INTEG CONDITION FOR 1062;
|
39
|
+
DECLARE CONTINUE HANDLER FOR ER_DUP_UNIQUE BEGIN
|
40
|
+
SET done = FALSE;
|
41
|
+
END;
|
42
|
+
|
43
|
+
DECLARE CONTINUE HANDLER FOR ER_INTEG BEGIN
|
44
|
+
SET done = TRUE;
|
45
|
+
END;
|
46
|
+
|
47
|
+
SET done = TRUE;
|
48
|
+
SELECT COUNT(*) INTO @count FROM #{quoted_table_name} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
|
49
|
+
-- Race condition here. If a concurrent INSERT is made after
|
50
|
+
-- the SELECT but before the INSERT below we'll get a duplicate
|
51
|
+
-- key error. But the handler above will take care of that.
|
52
|
+
IF @count > 0 THEN
|
53
|
+
-- UPDATE table_name SET b = b_SET WHERE a = a_SEL;
|
54
|
+
UPDATE #{quoted_table_name} SET #{update_column_definitions.map(&:to_setter).join(', ')} WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ')};
|
55
|
+
ELSE
|
56
|
+
-- INSERT INTO table_name (a, b) VALUES (k, data);
|
57
|
+
INSERT INTO #{quoted_table_name} (#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:to_setter_value).join(', ')});
|
58
|
+
END IF;
|
59
|
+
END;
|
60
|
+
UNTIL done END REPEAT;
|
61
|
+
END
|
62
|
+
})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,365 @@
|
|
1
|
+
class Upsert
|
2
|
+
class MergeFunction
|
3
|
+
# @private
|
4
|
+
module Postgresql
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def clear(connection)
|
11
|
+
# http://stackoverflow.com/questions/7622908/postgresql-drop-function-without-knowing-the-number-type-of-parameters
|
12
|
+
connection.execute(%{
|
13
|
+
CREATE OR REPLACE FUNCTION pg_temp.upsert_delfunc(text)
|
14
|
+
RETURNS void AS
|
15
|
+
$BODY$
|
16
|
+
DECLARE
|
17
|
+
_sql text;
|
18
|
+
BEGIN
|
19
|
+
FOR _sql IN
|
20
|
+
SELECT 'DROP FUNCTION ' || quote_ident(n.nspname)
|
21
|
+
|| '.' || quote_ident(p.proname)
|
22
|
+
|| '(' || pg_catalog.pg_get_function_identity_arguments(p.oid) || ');'
|
23
|
+
FROM pg_catalog.pg_proc p
|
24
|
+
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace
|
25
|
+
WHERE p.proname = $1
|
26
|
+
AND pg_catalog.pg_function_is_visible(p.oid) -- you may or may not want this
|
27
|
+
LOOP
|
28
|
+
EXECUTE _sql;
|
29
|
+
END LOOP;
|
30
|
+
END;
|
31
|
+
$BODY$
|
32
|
+
LANGUAGE plpgsql;
|
33
|
+
})
|
34
|
+
connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE '#{MergeFunction::NAME_PREFIX}%'}).each do |row|
|
35
|
+
k = row['proname']
|
36
|
+
next if k == 'upsert_delfunc'
|
37
|
+
Upsert.logger.info %{[upsert] Dropping function #{k.inspect}}
|
38
|
+
connection.execute %{SELECT pg_temp.upsert_delfunc('#{k}')}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :quoted_setter_names
|
44
|
+
attr_reader :quoted_selector_names
|
45
|
+
|
46
|
+
def initialize(controller, *args)
|
47
|
+
super
|
48
|
+
@quoted_setter_names = setter_keys.map { |k| connection.quote_ident k }
|
49
|
+
@quoted_selector_names = selector_keys.map { |k| connection.quote_ident k }
|
50
|
+
end
|
51
|
+
|
52
|
+
def execute(row)
|
53
|
+
use_pg_native? ? pg_native(row) : pg_function(row)
|
54
|
+
end
|
55
|
+
|
56
|
+
def pg_function(row)
|
57
|
+
values = []
|
58
|
+
values += row.selector.values
|
59
|
+
values += row.setter.values
|
60
|
+
hstore_delete_handlers.each do |hstore_delete_handler|
|
61
|
+
values << row.hstore_delete_keys.fetch(hstore_delete_handler.name, [])
|
62
|
+
end
|
63
|
+
Upsert.logger.debug do
|
64
|
+
%{[upsert]\n\tSelector: #{row.selector.inspect}\n\tSetter: #{row.setter.inspect}}
|
65
|
+
end
|
66
|
+
|
67
|
+
first_try = true
|
68
|
+
begin
|
69
|
+
create! if !@assume_function_exists && (connection.in_transaction? && !function_exists?)
|
70
|
+
execute_parameterized(sql, values.map { |v| connection.bind_value v })
|
71
|
+
rescue self.class::ERROR_CLASS => pg_error
|
72
|
+
if pg_error.message =~ /function #{name}.* does not exist/i
|
73
|
+
if first_try
|
74
|
+
Upsert.logger.info %{[upsert] Function #{name.inspect} went missing, trying to recreate}
|
75
|
+
first_try = false
|
76
|
+
create!
|
77
|
+
retry
|
78
|
+
end
|
79
|
+
Upsert.logger.info %{[upsert] Failed to create function #{name.inspect} for some reason}
|
80
|
+
raise pg_error
|
81
|
+
else
|
82
|
+
raise pg_error
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def function_exists?
|
88
|
+
@function_exists ||= controller.connection.execute("SELECT count(*) AS cnt FROM pg_proc WHERE lower(proname) = lower('#{name}')").first["cnt"].to_i > 0
|
89
|
+
end
|
90
|
+
|
91
|
+
# strangely ? can't be used as a placeholder
|
92
|
+
def sql
|
93
|
+
@sql ||= begin
|
94
|
+
bind_params = []
|
95
|
+
i = 1
|
96
|
+
(selector_keys.length + setter_keys.length).times do
|
97
|
+
bind_params << "$#{i}"
|
98
|
+
i += 1
|
99
|
+
end
|
100
|
+
hstore_delete_handlers.length.times do
|
101
|
+
bind_params << "$#{i}::text[]"
|
102
|
+
i += 1
|
103
|
+
end
|
104
|
+
%{SELECT #{name}(#{bind_params.join(', ')})}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def use_pg_native?
|
109
|
+
return @use_pg_native if defined?(@use_pg_native)
|
110
|
+
|
111
|
+
@use_pg_native = server_version >= 90500 && unique_index_on_selector?
|
112
|
+
Upsert.logger.warn "[upsert] WARNING: Not using native PG CONFLICT / UPDATE" unless @use_pg_native
|
113
|
+
@use_pg_native
|
114
|
+
end
|
115
|
+
|
116
|
+
def server_version
|
117
|
+
@server_version ||= Upsert::MergeFunction::Postgresql.extract_version(
|
118
|
+
controller.connection.execute("SHOW server_version").first["server_version"]
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Extracted from https://github.com/dr-itz/activerecord-jdbc-adapter/blob/master/lib/arjdbc/postgresql/adapter.rb
|
123
|
+
def self.extract_version(version_string)
|
124
|
+
# Use the same versioning format as jdbc-postgresql and libpq
|
125
|
+
# https://github.com/dr-itz/activerecord-jdbc-adapter/commit/fd79756374c62fa9d009995dd1914d780e6a3dbf
|
126
|
+
# https://github.com/postgres/postgres/blob/master/src/interfaces/libpq/fe-exec.c
|
127
|
+
if (match = version_string.match(/([\d\.]*\d).*?/))
|
128
|
+
version = match[1].split('.').map(&:to_i)
|
129
|
+
# PostgreSQL version representation does not have more than 4 digits
|
130
|
+
# From version 10 onwards, PG has changed its versioning policy to
|
131
|
+
# limit it to only 2 digits. i.e. in 10.x, 10 being the major
|
132
|
+
# version and x representing the patch release
|
133
|
+
# Refer to:
|
134
|
+
# https://www.postgresql.org/support/versioning/
|
135
|
+
# https://www.postgresql.org/docs/10/static/libpq-status.html -> PQserverVersion()
|
136
|
+
# for more info
|
137
|
+
|
138
|
+
if version.size >= 3
|
139
|
+
(version[0] * 100 + version[1]) * 100 + version[2]
|
140
|
+
elsif version.size == 2
|
141
|
+
if version[0] >= 10
|
142
|
+
version[0] * 100 * 100 + version[1]
|
143
|
+
else
|
144
|
+
(version[0] * 100 + version[1]) * 100
|
145
|
+
end
|
146
|
+
elsif version.size == 1
|
147
|
+
version[0] * 100 * 100
|
148
|
+
else
|
149
|
+
0
|
150
|
+
end
|
151
|
+
else
|
152
|
+
0
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def unique_index_columns
|
157
|
+
if table_name.is_a?(Array) && table_name.length > 1
|
158
|
+
schema_argument = '$2'
|
159
|
+
table_name_arguments = table_name
|
160
|
+
else
|
161
|
+
schema_argument = 'ANY(current_schemas(true)::text[])'
|
162
|
+
table_name_arguments = [*table_name]
|
163
|
+
end
|
164
|
+
|
165
|
+
table_name_arguments.reverse!
|
166
|
+
|
167
|
+
execute_parameterized(
|
168
|
+
%{
|
169
|
+
SELECT
|
170
|
+
ARRAY(
|
171
|
+
SELECT pg_get_indexdef(pg_index.indexrelid, k + 1, TRUE)
|
172
|
+
FROM
|
173
|
+
generate_subscripts(pg_index.indkey, 1) AS k
|
174
|
+
ORDER BY k
|
175
|
+
) AS index_columns
|
176
|
+
FROM pg_index
|
177
|
+
JOIN pg_class AS idx ON idx.oid = pg_index.indexrelid
|
178
|
+
JOIN pg_class AS tbl ON tbl.oid = pg_index.indrelid
|
179
|
+
JOIN pg_namespace ON pg_namespace.oid = idx.relnamespace
|
180
|
+
WHERE pg_index.indisunique IS TRUE AND pg_namespace.nspname = #{schema_argument} AND tbl.relname = $1
|
181
|
+
},
|
182
|
+
table_name_arguments
|
183
|
+
)
|
184
|
+
end
|
185
|
+
|
186
|
+
def pg_native(row)
|
187
|
+
bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
|
188
|
+
# TODO: Is this needed?
|
189
|
+
row_syntax = server_version >= 100 ? "ROW" : ""
|
190
|
+
|
191
|
+
upsert_sql = %{
|
192
|
+
INSERT INTO #{quoted_table_name} (#{quoted_setter_names.join(',')})
|
193
|
+
VALUES (#{insert_bind_placeholders(row).join(', ')})
|
194
|
+
ON CONFLICT(#{quoted_selector_names.join(', ')})
|
195
|
+
DO UPDATE SET #{quoted_setter_names.zip(conflict_bind_placeholders(row)).map { |n, v| "#{n} = #{v}" }.join(', ')}
|
196
|
+
}
|
197
|
+
|
198
|
+
execute_parameterized(upsert_sql, bind_setter_values)
|
199
|
+
end
|
200
|
+
|
201
|
+
def hstore_delete_function(sql, row, column_definition)
|
202
|
+
parts = []
|
203
|
+
if row.hstore_delete_keys.key?(column_definition.name)
|
204
|
+
parts << "DELETE("
|
205
|
+
end
|
206
|
+
parts << sql
|
207
|
+
if row.hstore_delete_keys.key?(column_definition.name)
|
208
|
+
keys = row.hstore_delete_keys[column_definition.name].map { |k| "'#{k.to_s.gsub("'", "\\'")}'" }
|
209
|
+
parts << ", ARRAY[#{keys.join(', ')}])"
|
210
|
+
end
|
211
|
+
|
212
|
+
parts.join(" ")
|
213
|
+
end
|
214
|
+
|
215
|
+
def insert_bind_placeholders(row)
|
216
|
+
if row.hstore_delete_keys.empty?
|
217
|
+
@insert_bind_placeholders ||= setter_column_definitions.each_with_index.map do |column_definition, i|
|
218
|
+
if column_definition.hstore?
|
219
|
+
"CAST($#{i + 1} AS hstore)"
|
220
|
+
else
|
221
|
+
"$#{i + 1}"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
else
|
225
|
+
setter_column_definitions.each_with_index.map do |column_definition, i|
|
226
|
+
idx = i + 1
|
227
|
+
if column_definition.hstore?
|
228
|
+
hstore_delete_function("CAST($#{idx} AS hstore)", row, column_definition)
|
229
|
+
else
|
230
|
+
"$#{idx}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def conflict_bind_placeholders(row)
|
237
|
+
if row.hstore_delete_keys.empty?
|
238
|
+
@conflict_bind_placeholders ||= setter_column_definitions.each_with_index.map do |column_definition, i|
|
239
|
+
idx = i + 1
|
240
|
+
if column_definition.hstore?
|
241
|
+
"CASE WHEN #{quoted_table_name}.#{column_definition.quoted_name} IS NULL THEN CAST($#{idx} AS hstore) ELSE" \
|
242
|
+
+ " (#{quoted_table_name}.#{column_definition.quoted_name} || CAST($#{idx} AS hstore))" \
|
243
|
+
+ " END"
|
244
|
+
else
|
245
|
+
"$#{idx}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
else
|
249
|
+
setter_column_definitions.each_with_index.map do |column_definition, i|
|
250
|
+
idx = i + 1
|
251
|
+
if column_definition.hstore?
|
252
|
+
"CASE WHEN #{quoted_table_name}.#{column_definition.quoted_name} IS NULL THEN " \
|
253
|
+
+ hstore_delete_function("CAST($#{idx} AS hstore)", row, column_definition) \
|
254
|
+
+ " ELSE " \
|
255
|
+
+ hstore_delete_function("(#{quoted_table_name}.#{column_definition.quoted_name} || CAST($#{idx} AS hstore))", row, column_definition) \
|
256
|
+
+ " END"
|
257
|
+
else
|
258
|
+
"$#{idx}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
class HstoreDeleteHandler
|
265
|
+
attr_reader :merge_function
|
266
|
+
attr_reader :column_definition
|
267
|
+
def initialize(merge_function, column_definition)
|
268
|
+
@merge_function = merge_function
|
269
|
+
@column_definition = column_definition
|
270
|
+
end
|
271
|
+
def name
|
272
|
+
column_definition.name
|
273
|
+
end
|
274
|
+
def to_arg
|
275
|
+
"#{quoted_name} text[]"
|
276
|
+
end
|
277
|
+
# use coalesce(foo, '{}':text[])
|
278
|
+
def to_setter
|
279
|
+
"#{column_definition.quoted_name} = DELETE(#{column_definition.quoted_name}, #{quoted_name})"
|
280
|
+
end
|
281
|
+
def to_pgsql
|
282
|
+
%{
|
283
|
+
IF array_length(#{quoted_name}, 1) > 0 THEN
|
284
|
+
UPDATE #{merge_function.quoted_table_name} SET #{to_setter}
|
285
|
+
WHERE #{merge_function.selector_column_definitions.map(&:to_selector).join(' AND ') };
|
286
|
+
END IF;
|
287
|
+
}.gsub(/\s+/, ' ')
|
288
|
+
end
|
289
|
+
private
|
290
|
+
def quoted_name
|
291
|
+
@quoted_name ||= merge_function.connection.quote_ident "_delete_#{column_definition.name}"
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
def hstore_delete_handlers
|
296
|
+
@hstore_delete_handlers ||= setter_column_definitions.select do |column_definition|
|
297
|
+
column_definition.hstore?
|
298
|
+
end.map do |column_definition|
|
299
|
+
HstoreDeleteHandler.new self, column_definition
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
def selector_column_definitions
|
304
|
+
column_definitions.select { |cd| selector_keys.include?(cd.name) }
|
305
|
+
end
|
306
|
+
|
307
|
+
def setter_column_definitions
|
308
|
+
column_definitions.select { |cd| setter_keys.include?(cd.name) }
|
309
|
+
end
|
310
|
+
|
311
|
+
def update_column_definitions
|
312
|
+
setter_column_definitions.select { |cd| cd.name !~ CREATED_COL_REGEX }
|
313
|
+
end
|
314
|
+
|
315
|
+
# the "canonical example" from http://www.postgresql.org/docs/9.1/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
|
316
|
+
# differentiate between selector and setter
|
317
|
+
def create!
|
318
|
+
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(', ')}"
|
319
|
+
first_try = true
|
320
|
+
connection.execute(%{
|
321
|
+
CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg) + hstore_delete_handlers.map(&:to_arg)).join(', ')}) RETURNS VOID AS
|
322
|
+
$$
|
323
|
+
DECLARE
|
324
|
+
first_try INTEGER := 1;
|
325
|
+
BEGIN
|
326
|
+
LOOP
|
327
|
+
-- first try to update the key
|
328
|
+
UPDATE #{quoted_table_name} SET #{update_column_definitions.map(&:to_setter).join(', ')}
|
329
|
+
WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
|
330
|
+
IF found THEN
|
331
|
+
#{hstore_delete_handlers.map(&:to_pgsql).join(' ')}
|
332
|
+
RETURN;
|
333
|
+
END IF;
|
334
|
+
-- not there, so try to insert the key
|
335
|
+
-- if someone else inserts the same key concurrently,
|
336
|
+
-- we could get a unique-key failure
|
337
|
+
BEGIN
|
338
|
+
INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:to_setter_value).join(', ')});
|
339
|
+
#{hstore_delete_handlers.map(&:to_pgsql).join(' ')}
|
340
|
+
RETURN;
|
341
|
+
EXCEPTION WHEN unique_violation THEN
|
342
|
+
-- seamusabshere 9/20/12 only retry once
|
343
|
+
IF (first_try = 1) THEN
|
344
|
+
first_try := 0;
|
345
|
+
ELSE
|
346
|
+
RETURN;
|
347
|
+
END IF;
|
348
|
+
-- Do nothing, and loop to try the UPDATE again.
|
349
|
+
END;
|
350
|
+
END LOOP;
|
351
|
+
END;
|
352
|
+
$$
|
353
|
+
LANGUAGE plpgsql;
|
354
|
+
})
|
355
|
+
rescue
|
356
|
+
if first_try and $!.message =~ /tuple concurrently updated/
|
357
|
+
first_try = false
|
358
|
+
retry
|
359
|
+
else
|
360
|
+
raise $!
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|