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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.ruby-version +1 -0
  4. data/.standard.yml +1 -0
  5. data/.travis.yml +63 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG +265 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE +24 -0
  10. data/README.md +411 -0
  11. data/Rakefile +54 -0
  12. data/lib/upsert.rb +284 -0
  13. data/lib/upsert/active_record_upsert.rb +12 -0
  14. data/lib/upsert/binary.rb +8 -0
  15. data/lib/upsert/column_definition.rb +79 -0
  16. data/lib/upsert/column_definition/mysql.rb +24 -0
  17. data/lib/upsert/column_definition/postgresql.rb +66 -0
  18. data/lib/upsert/column_definition/sqlite3.rb +34 -0
  19. data/lib/upsert/connection.rb +37 -0
  20. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
  21. data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
  22. data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
  23. data/lib/upsert/connection/Mysql2_Client.rb +76 -0
  24. data/lib/upsert/connection/PG_Connection.rb +35 -0
  25. data/lib/upsert/connection/SQLite3_Database.rb +28 -0
  26. data/lib/upsert/connection/jdbc.rb +105 -0
  27. data/lib/upsert/connection/postgresql.rb +24 -0
  28. data/lib/upsert/connection/sqlite3.rb +19 -0
  29. data/lib/upsert/merge_function.rb +73 -0
  30. data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
  31. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
  32. data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
  33. data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
  34. data/lib/upsert/merge_function/PG_Connection.rb +26 -0
  35. data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
  36. data/lib/upsert/merge_function/mysql.rb +66 -0
  37. data/lib/upsert/merge_function/postgresql.rb +365 -0
  38. data/lib/upsert/merge_function/sqlite3.rb +43 -0
  39. data/lib/upsert/row.rb +59 -0
  40. data/lib/upsert/version.rb +3 -0
  41. data/spec/active_record_upsert_spec.rb +26 -0
  42. data/spec/binary_spec.rb +21 -0
  43. data/spec/correctness_spec.rb +190 -0
  44. data/spec/database_functions_spec.rb +106 -0
  45. data/spec/database_spec.rb +121 -0
  46. data/spec/hstore_spec.rb +249 -0
  47. data/spec/jruby_spec.rb +9 -0
  48. data/spec/logger_spec.rb +52 -0
  49. data/spec/misc/get_postgres_reserved_words.rb +12 -0
  50. data/spec/misc/mysql_reserved.txt +226 -0
  51. data/spec/misc/pg_reserved.txt +742 -0
  52. data/spec/multibyte_spec.rb +27 -0
  53. data/spec/postgresql_spec.rb +94 -0
  54. data/spec/precision_spec.rb +11 -0
  55. data/spec/reserved_words_spec.rb +50 -0
  56. data/spec/sequel_spec.rb +57 -0
  57. data/spec/spec_helper.rb +417 -0
  58. data/spec/speed_spec.rb +44 -0
  59. data/spec/threaded_spec.rb +57 -0
  60. data/spec/timezones_spec.rb +58 -0
  61. data/spec/type_safety_spec.rb +12 -0
  62. data/travis/install_postgres.sh +18 -0
  63. data/travis/run_docker_db.sh +20 -0
  64. data/travis/tune_mysql.sh +7 -0
  65. data/upsert-java.gemspec +13 -0
  66. data/upsert.gemspec +11 -0
  67. data/upsert.gemspec.common +107 -0
  68. 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,19 @@
1
+ class Upsert
2
+ class Connection
3
+ # @private
4
+ module Sqlite3
5
+ def bind_value(v)
6
+ case v
7
+ when BigDecimal
8
+ v.to_s('F')
9
+ when TrueClass
10
+ 't'
11
+ when FalseClass
12
+ 'f'
13
+ else
14
+ super
15
+ end
16
+ end
17
+ end
18
+ end
19
+ 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,10 @@
1
+ require 'upsert/merge_function/sqlite3'
2
+
3
+ class Upsert
4
+ class MergeFunction
5
+ # @private
6
+ class Java_OrgSqlite_Conn < MergeFunction
7
+ include Sqlite3
8
+ end
9
+ end
10
+ 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,10 @@
1
+ require 'upsert/merge_function/sqlite3'
2
+
3
+ class Upsert
4
+ class MergeFunction
5
+ # @private
6
+ class SQLite3_Database < MergeFunction
7
+ include Sqlite3
8
+ end
9
+ end
10
+ 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