upsert 2.1.0 → 2.9.10
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 +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
|