upsert 2.9.10-java

Sign up to get free protection for your applications and to get access to all the features.
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 +20 -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 +14 -0
  66. data/upsert.gemspec +13 -0
  67. data/upsert.gemspec.common +106 -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