upsert 2.1.0 → 2.9.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -0
- data/.standard.yml +1 -0
- data/.travis.yml +60 -12
- data/CHANGELOG +39 -0
- data/Gemfile +12 -1
- data/LICENSE +3 -1
- data/README.md +47 -6
- data/Rakefile +7 -1
- data/lib/upsert.rb +54 -11
- data/lib/upsert/column_definition/mysql.rb +2 -2
- data/lib/upsert/column_definition/postgresql.rb +9 -8
- data/lib/upsert/column_definition/sqlite3.rb +3 -3
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +11 -5
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
- data/lib/upsert/connection/PG_Connection.rb +10 -1
- data/lib/upsert/connection/jdbc.rb +20 -1
- data/lib/upsert/connection/postgresql.rb +2 -3
- data/lib/upsert/merge_function.rb +5 -4
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
- data/lib/upsert/merge_function/PG_Connection.rb +11 -42
- data/lib/upsert/merge_function/postgresql.rb +215 -1
- data/lib/upsert/merge_function/sqlite3.rb +10 -0
- data/lib/upsert/version.rb +1 -1
- data/spec/active_record_upsert_spec.rb +10 -0
- data/spec/correctness_spec.rb +34 -5
- data/spec/database_functions_spec.rb +16 -9
- data/spec/database_spec.rb +7 -0
- data/spec/hstore_spec.rb +56 -55
- data/spec/jruby_spec.rb +9 -0
- data/spec/logger_spec.rb +8 -6
- data/spec/postgresql_spec.rb +94 -0
- data/spec/reserved_words_spec.rb +21 -17
- data/spec/sequel_spec.rb +26 -7
- data/spec/spec_helper.rb +251 -92
- data/spec/speed_spec.rb +3 -32
- data/spec/threaded_spec.rb +35 -12
- data/spec/type_safety_spec.rb +2 -1
- 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 +9 -57
- data/upsert.gemspec.common +107 -0
- metadata +53 -40
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +0 -15
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +0 -39
@@ -3,9 +3,9 @@ class Upsert
|
|
3
3
|
# @private
|
4
4
|
class Sqlite3 < ColumnDefinition
|
5
5
|
class << self
|
6
|
-
def all(connection,
|
6
|
+
def all(connection, quoted_table_name)
|
7
7
|
# activerecord-3.2.13/lib/active_record/connection_adapters/sqlite_adapter.rb
|
8
|
-
connection.execute("PRAGMA table_info(#{
|
8
|
+
connection.execute("PRAGMA table_info(#{quoted_table_name})").map do |row|#, 'SCHEMA').to_hash
|
9
9
|
if connection.metal.respond_to?(:results_as_hash) and not connection.metal.results_as_hash
|
10
10
|
row = {'name' => row[1], 'type' => row[2], 'dflt_value' => row[4]}
|
11
11
|
end
|
@@ -25,7 +25,7 @@ class Upsert
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
-
|
28
|
+
|
29
29
|
def equality(left, right)
|
30
30
|
"(#{left} IS #{right} OR (#{left} IS NULL AND #{right} IS NULL))"
|
31
31
|
end
|
@@ -6,16 +6,22 @@ class Upsert
|
|
6
6
|
class Java_ComMysqlJdbc_JDBC4Connection < Connection
|
7
7
|
include Jdbc
|
8
8
|
|
9
|
-
# ? backtick?
|
10
9
|
def quote_ident(k)
|
11
|
-
|
10
|
+
if metal.useAnsiQuotedIdentifiers
|
11
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
12
|
+
else
|
13
|
+
# Escape backticks by doubling them. Ref http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
|
14
|
+
BACKTICK + k.to_s.gsub(BACKTICK, BACKTICK + BACKTICK) + BACKTICK
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
def bind_value(v)
|
15
19
|
case v
|
16
|
-
when
|
17
|
-
|
18
|
-
|
20
|
+
when DateTime, Time
|
21
|
+
date = v.utc
|
22
|
+
java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
|
23
|
+
when Date
|
24
|
+
java.time.LocalDate.of(v.year, v.month, v.day)
|
19
25
|
else
|
20
26
|
super
|
21
27
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require_relative "jdbc"
|
2
|
+
require_relative "postgresql"
|
3
|
+
|
4
|
+
class Upsert
|
5
|
+
class Connection
|
6
|
+
# @private
|
7
|
+
class Java_OrgPostgresqlJdbc_PgConnection < Connection
|
8
|
+
include Jdbc
|
9
|
+
include Postgresql
|
10
|
+
|
11
|
+
def quote_ident(k)
|
12
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
13
|
+
end
|
14
|
+
|
15
|
+
def in_transaction?
|
16
|
+
# https://github.com/kares/activerecord-jdbc-adapter/commit/4d6e0e0c52d12b0166810dffc9f898141a23bee6
|
17
|
+
![0, 4].include?(metal.get_transaction_state)
|
18
|
+
end
|
19
|
+
|
20
|
+
def bind_value(v)
|
21
|
+
case v
|
22
|
+
when DateTime, Time
|
23
|
+
date = v.utc
|
24
|
+
java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
|
25
|
+
when Date
|
26
|
+
java.time.LocalDate.of(v.year, v.month, v.day)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -1,12 +1,17 @@
|
|
1
|
+
require_relative "postgresql"
|
2
|
+
|
1
3
|
class Upsert
|
2
4
|
class Connection
|
3
5
|
# @private
|
4
6
|
class PG_Connection < Connection
|
5
7
|
include Postgresql
|
6
|
-
|
8
|
+
|
7
9
|
def execute(sql, params = nil)
|
8
10
|
if params
|
9
11
|
# Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
12
|
+
# The following will blow up if you pass a value that cannot be automatically type-casted,
|
13
|
+
# such as passing a string to an integer field. You'll get an error something along the
|
14
|
+
# lines of: "invalid input syntax for <type>: <value>"
|
10
15
|
metal.exec sql, convert_binary(params)
|
11
16
|
else
|
12
17
|
Upsert.logger.debug { %{[upsert] #{sql}} }
|
@@ -21,6 +26,10 @@ class Upsert
|
|
21
26
|
def binary(v)
|
22
27
|
{ :value => v.value, :format => 1 }
|
23
28
|
end
|
29
|
+
|
30
|
+
def in_transaction?
|
31
|
+
![PG::PQTRANS_IDLE, PG::PQTRANS_UNKNOWN].include?(metal.transaction_status)
|
32
|
+
end
|
24
33
|
end
|
25
34
|
end
|
26
35
|
end
|
@@ -4,11 +4,15 @@ class Upsert
|
|
4
4
|
module Jdbc
|
5
5
|
# /Users/seamusabshere/.rvm/gems/jruby-head/gems/activerecord-jdbc-adapter-1.2.2.1/src/java/arjdbc/jdbc/RubyJdbcConnection.java
|
6
6
|
GETTER = {
|
7
|
+
java.sql.Types::CHAR => 'getString',
|
7
8
|
java.sql.Types::VARCHAR => 'getString',
|
8
9
|
java.sql.Types::OTHER => 'getString', # ?! i guess unicode text?
|
9
10
|
java.sql.Types::BINARY => 'getBlob',
|
10
11
|
java.sql.Types::LONGVARCHAR => 'getString',
|
12
|
+
java.sql.Types::BIGINT => 'getLong',
|
11
13
|
java.sql.Types::INTEGER => 'getInt',
|
14
|
+
java.sql.Types::REAL => "getLong",
|
15
|
+
java.sql.Types::ARRAY => ->(r, i){ r.getArray(i).array.to_ary }
|
12
16
|
}
|
13
17
|
java.sql.Types.constants.each do |type_name|
|
14
18
|
i = java.sql.Types.const_get type_name
|
@@ -22,6 +26,7 @@ class Upsert
|
|
22
26
|
'TrueClass' => 'setBoolean',
|
23
27
|
'FalseClass' => 'setBoolean',
|
24
28
|
'Fixnum' => 'setInt',
|
29
|
+
'Integer' => 'setInt'
|
25
30
|
)
|
26
31
|
|
27
32
|
def binary(v)
|
@@ -34,16 +39,24 @@ class Upsert
|
|
34
39
|
setters = self.class.const_get(:SETTER)
|
35
40
|
statement = metal.prepareStatement sql
|
36
41
|
params.each_with_index do |v, i|
|
42
|
+
if v.is_a?(Fixnum) && v > 2_147_483_647
|
43
|
+
statement.setLong i+1, v
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
37
47
|
case v
|
38
48
|
when Upsert::Binary
|
39
49
|
statement.setBytes i+1, binary(v)
|
40
|
-
when BigDecimal
|
50
|
+
when Float, BigDecimal
|
41
51
|
statement.setBigDecimal i+1, java.math.BigDecimal.new(v.to_s)
|
42
52
|
when NilClass
|
43
53
|
# http://stackoverflow.com/questions/4243513/why-does-preparedstatement-setnull-requires-sqltype
|
44
54
|
statement.setObject i+1, nil
|
55
|
+
when java.time.LocalDateTime, java.time.Instant, java.time.LocalDate
|
56
|
+
statement.setObject i+1, v
|
45
57
|
else
|
46
58
|
setter = setters[v.class.name]
|
59
|
+
Upsert.logger.debug { "Setting [#{v.class}, #{v}] via #{setter}" }
|
47
60
|
statement.send setter, i+1, v
|
48
61
|
end
|
49
62
|
end
|
@@ -72,6 +85,8 @@ class Upsert
|
|
72
85
|
column_name, getter = cg
|
73
86
|
if getter == 'getNull'
|
74
87
|
row[column_name] = nil
|
88
|
+
elsif getter.respond_to?(:call)
|
89
|
+
row[column_name] = getter.call(raw_result, i)
|
75
90
|
else
|
76
91
|
row[column_name] = raw_result.send(getter, i)
|
77
92
|
end
|
@@ -81,6 +96,10 @@ class Upsert
|
|
81
96
|
statement.close
|
82
97
|
result
|
83
98
|
end
|
99
|
+
|
100
|
+
def in_transaction?
|
101
|
+
raise "Not implemented"
|
102
|
+
end
|
84
103
|
end
|
85
104
|
end
|
86
105
|
end
|
@@ -8,9 +8,8 @@ class Upsert
|
|
8
8
|
# pg array escaping lifted from https://github.com/tlconnor/activerecord-postgres-array/blob/master/lib/activerecord-postgres-array/array.rb
|
9
9
|
'{' + v.map do |vv|
|
10
10
|
vv = vv.to_s.dup
|
11
|
-
vv.gsub!
|
12
|
-
vv.gsub!
|
13
|
-
vv.gsub! /"/, '\"'
|
11
|
+
vv.gsub!(/\\/, '\&\&')
|
12
|
+
vv.gsub!(/"/, '\"')
|
14
13
|
%{"#{vv}"}
|
15
14
|
end.join(',') + '}'
|
16
15
|
when Hash
|
@@ -11,11 +11,11 @@ class Upsert
|
|
11
11
|
def unique_name(table_name, selector_keys, setter_keys)
|
12
12
|
parts = [
|
13
13
|
NAME_PREFIX,
|
14
|
-
table_name,
|
14
|
+
[*table_name].join("_").gsub(/[^\w_]+/, "_"),
|
15
15
|
'SEL',
|
16
|
-
selector_keys.join('_A_'),
|
16
|
+
selector_keys.join('_A_').gsub(" ","_"),
|
17
17
|
'SET',
|
18
|
-
setter_keys.join('_A_')
|
18
|
+
setter_keys.join('_A_').gsub(" ","_")
|
19
19
|
].join('_')
|
20
20
|
if parts.length > MAX_NAME_LENGTH
|
21
21
|
# maybe i should md5 instead
|
@@ -35,8 +35,9 @@ class Upsert
|
|
35
35
|
@controller = controller
|
36
36
|
@selector_keys = selector_keys
|
37
37
|
@setter_keys = setter_keys
|
38
|
+
@assume_function_exists = assume_function_exists
|
38
39
|
validate!
|
39
|
-
create! unless assume_function_exists
|
40
|
+
create! unless @assume_function_exists
|
40
41
|
end
|
41
42
|
|
42
43
|
def name
|
@@ -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
|
@@ -4,52 +4,21 @@ class Upsert
|
|
4
4
|
class MergeFunction
|
5
5
|
# @private
|
6
6
|
class PG_Connection < MergeFunction
|
7
|
+
ERROR_CLASS = PG::Error
|
7
8
|
include Postgresql
|
8
9
|
|
9
|
-
def
|
10
|
-
|
11
|
-
values = []
|
12
|
-
values += row.selector.values
|
13
|
-
values += row.setter.values
|
14
|
-
hstore_delete_handlers.each do |hstore_delete_handler|
|
15
|
-
values << row.hstore_delete_keys.fetch(hstore_delete_handler.name, [])
|
16
|
-
end
|
17
|
-
Upsert.logger.debug do
|
18
|
-
%{[upsert]\n\tSelector: #{row.selector.inspect}\n\tSetter: #{row.setter.inspect}}
|
19
|
-
end
|
20
|
-
begin
|
21
|
-
connection.execute sql, values.map { |v| connection.bind_value v }
|
22
|
-
rescue PG::Error => pg_error
|
23
|
-
if pg_error.message =~ /function #{name}.* 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 pg_error
|
32
|
-
end
|
33
|
-
else
|
34
|
-
raise pg_error
|
35
|
-
end
|
36
|
-
end
|
10
|
+
def execute_parameterized(query, args = [])
|
11
|
+
controller.connection.execute(query, args)
|
37
12
|
end
|
38
13
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
48
|
-
hstore_delete_handlers.length.times do
|
49
|
-
bind_params << "$#{i}::text[]"
|
50
|
-
i += 1
|
51
|
-
end
|
52
|
-
%{SELECT #{name}(#{bind_params.join(', ')})}
|
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
|
53
22
|
end
|
54
23
|
end
|
55
24
|
end
|
@@ -40,13 +40,227 @@ class Upsert
|
|
40
40
|
end
|
41
41
|
end
|
42
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
|
43
92
|
def sql
|
44
93
|
@sql ||= begin
|
45
|
-
bind_params =
|
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
|
46
104
|
%{SELECT #{name}(#{bind_params.join(', ')})}
|
47
105
|
end
|
48
106
|
end
|
49
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
|
+
|
50
264
|
class HstoreDeleteHandler
|
51
265
|
attr_reader :merge_function
|
52
266
|
attr_reader :column_definition
|