upsert 2.1.0 → 2.9.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +1 -0
  4. data/.travis.yml +60 -12
  5. data/CHANGELOG +39 -0
  6. data/Gemfile +12 -1
  7. data/LICENSE +3 -1
  8. data/README.md +47 -6
  9. data/Rakefile +7 -1
  10. data/lib/upsert.rb +54 -11
  11. data/lib/upsert/column_definition/mysql.rb +2 -2
  12. data/lib/upsert/column_definition/postgresql.rb +9 -8
  13. data/lib/upsert/column_definition/sqlite3.rb +3 -3
  14. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +11 -5
  15. data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
  16. data/lib/upsert/connection/PG_Connection.rb +10 -1
  17. data/lib/upsert/connection/jdbc.rb +20 -1
  18. data/lib/upsert/connection/postgresql.rb +2 -3
  19. data/lib/upsert/merge_function.rb +5 -4
  20. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
  21. data/lib/upsert/merge_function/PG_Connection.rb +11 -42
  22. data/lib/upsert/merge_function/postgresql.rb +215 -1
  23. data/lib/upsert/merge_function/sqlite3.rb +10 -0
  24. data/lib/upsert/version.rb +1 -1
  25. data/spec/active_record_upsert_spec.rb +10 -0
  26. data/spec/correctness_spec.rb +34 -5
  27. data/spec/database_functions_spec.rb +16 -9
  28. data/spec/database_spec.rb +7 -0
  29. data/spec/hstore_spec.rb +56 -55
  30. data/spec/jruby_spec.rb +9 -0
  31. data/spec/logger_spec.rb +8 -6
  32. data/spec/postgresql_spec.rb +94 -0
  33. data/spec/reserved_words_spec.rb +21 -17
  34. data/spec/sequel_spec.rb +26 -7
  35. data/spec/spec_helper.rb +251 -92
  36. data/spec/speed_spec.rb +3 -32
  37. data/spec/threaded_spec.rb +35 -12
  38. data/spec/type_safety_spec.rb +2 -1
  39. data/travis/install_postgres.sh +18 -0
  40. data/travis/run_docker_db.sh +20 -0
  41. data/travis/tune_mysql.sh +7 -0
  42. data/upsert-java.gemspec +13 -0
  43. data/upsert.gemspec +9 -57
  44. data/upsert.gemspec.common +107 -0
  45. metadata +53 -40
  46. data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +0 -15
  47. 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, table_name)
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(#{connection.quote_ident(table_name)})").map do |row|#, 'SCHEMA').to_hash
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
- DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
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 Time, DateTime
17
- # mysql doesn't like it when you send timezone to a datetime
18
- Upsert.utc_iso8601 v, false
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 execute(row)
10
- first_try = true
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
- # strangely ? can't be used as a placeholder
40
- def sql
41
- @sql ||= begin
42
- bind_params = []
43
- i = 1
44
- (selector_keys.length + setter_keys.length).times do
45
- bind_params << "$#{i}"
46
- i += 1
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 = Array.new(selector_keys.length + setter_keys.length, '?') + Array.new(hstore_delete_handlers.length, '?::text[]')
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