upsert 2.9.9-universal-java-11
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.
- 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
|