upsert 2.2.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 +54 -31
- data/CHANGELOG +16 -0
- data/Gemfile +12 -1
- data/LICENSE +3 -1
- data/README.md +43 -8
- data/Rakefile +7 -1
- data/lib/upsert.rb +49 -7
- 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 +5 -3
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
- data/lib/upsert/connection/PG_Connection.rb +5 -0
- data/lib/upsert/connection/jdbc.rb +7 -1
- data/lib/upsert/connection/postgresql.rb +2 -3
- data/lib/upsert/merge_function.rb +3 -2
- data/lib/upsert/merge_function/{Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb → Java_OrgPostgresqlJdbc_PgConnection.rb} +2 -2
- data/lib/upsert/merge_function/PG_Connection.rb +2 -2
- data/lib/upsert/merge_function/postgresql.rb +81 -19
- data/lib/upsert/merge_function/sqlite3.rb +10 -0
- data/lib/upsert/version.rb +1 -1
- data/spec/correctness_spec.rb +20 -5
- data/spec/database_functions_spec.rb +6 -2
- data/spec/hstore_spec.rb +53 -38
- data/spec/logger_spec.rb +1 -1
- data/spec/postgresql_spec.rb +81 -3
- data/spec/reserved_words_spec.rb +18 -14
- data/spec/sequel_spec.rb +16 -7
- data/spec/spec_helper.rb +238 -111
- data/spec/speed_spec.rb +3 -33
- data/spec/threaded_spec.rb +35 -12
- data/spec/type_safety_spec.rb +2 -1
- data/travis/run_docker_db.sh +20 -0
- data/upsert-java.gemspec +13 -0
- data/upsert.gemspec +9 -58
- data/upsert.gemspec.common +107 -0
- metadata +39 -46
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +0 -20
@@ -17,9 +17,11 @@ class Upsert
|
|
17
17
|
|
18
18
|
def bind_value(v)
|
19
19
|
case v
|
20
|
-
when
|
21
|
-
|
22
|
-
|
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)
|
23
25
|
else
|
24
26
|
super
|
25
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,3 +1,5 @@
|
|
1
|
+
require_relative "postgresql"
|
2
|
+
|
1
3
|
class Upsert
|
2
4
|
class Connection
|
3
5
|
# @private
|
@@ -7,6 +9,9 @@ class Upsert
|
|
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}} }
|
@@ -4,12 +4,14 @@ 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',
|
11
12
|
java.sql.Types::BIGINT => 'getLong',
|
12
13
|
java.sql.Types::INTEGER => 'getInt',
|
14
|
+
java.sql.Types::REAL => "getLong",
|
13
15
|
java.sql.Types::ARRAY => ->(r, i){ r.getArray(i).array.to_ary }
|
14
16
|
}
|
15
17
|
java.sql.Types.constants.each do |type_name|
|
@@ -24,6 +26,7 @@ class Upsert
|
|
24
26
|
'TrueClass' => 'setBoolean',
|
25
27
|
'FalseClass' => 'setBoolean',
|
26
28
|
'Fixnum' => 'setInt',
|
29
|
+
'Integer' => 'setInt'
|
27
30
|
)
|
28
31
|
|
29
32
|
def binary(v)
|
@@ -44,13 +47,16 @@ class Upsert
|
|
44
47
|
case v
|
45
48
|
when Upsert::Binary
|
46
49
|
statement.setBytes i+1, binary(v)
|
47
|
-
when BigDecimal
|
50
|
+
when Float, BigDecimal
|
48
51
|
statement.setBigDecimal i+1, java.math.BigDecimal.new(v.to_s)
|
49
52
|
when NilClass
|
50
53
|
# http://stackoverflow.com/questions/4243513/why-does-preparedstatement-setnull-requires-sqltype
|
51
54
|
statement.setObject i+1, nil
|
55
|
+
when java.time.LocalDateTime, java.time.Instant, java.time.LocalDate
|
56
|
+
statement.setObject i+1, v
|
52
57
|
else
|
53
58
|
setter = setters[v.class.name]
|
59
|
+
Upsert.logger.debug { "Setting [#{v.class}, #{v}] via #{setter}" }
|
54
60
|
statement.send setter, i+1, v
|
55
61
|
end
|
56
62
|
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,7 +11,7 @@ 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
16
|
selector_keys.join('_A_').gsub(" ","_"),
|
17
17
|
'SET',
|
@@ -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
|
@@ -3,7 +3,7 @@ require 'upsert/merge_function/postgresql'
|
|
3
3
|
class Upsert
|
4
4
|
class MergeFunction
|
5
5
|
# @private
|
6
|
-
class
|
6
|
+
class Java_OrgPostgresqlJdbc_PgConnection < MergeFunction
|
7
7
|
ERROR_CLASS = org.postgresql.util.PSQLException
|
8
8
|
include Postgresql
|
9
9
|
|
@@ -18,7 +18,7 @@ class Upsert
|
|
18
18
|
|
19
19
|
def unique_index_on_selector?
|
20
20
|
return @unique_index_on_selector if defined?(@unique_index_on_selector)
|
21
|
-
@unique_index_on_selector =
|
21
|
+
@unique_index_on_selector = unique_index_columns.any? do |row|
|
22
22
|
row["index_columns"].sort == selector_keys.sort
|
23
23
|
end
|
24
24
|
end
|
@@ -15,9 +15,9 @@ class Upsert
|
|
15
15
|
return @unique_index_on_selector if defined?(@unique_index_on_selector)
|
16
16
|
|
17
17
|
type_map = PG::TypeMapByColumn.new([PG::TextDecoder::Array.new])
|
18
|
-
|
18
|
+
res = unique_index_columns.tap { |r| r.type_map = type_map }
|
19
19
|
|
20
|
-
@unique_index_on_selector =
|
20
|
+
@unique_index_on_selector = res.values.any? do |row|
|
21
21
|
row.first.sort == selector_keys.sort
|
22
22
|
end
|
23
23
|
end
|
@@ -66,7 +66,7 @@ class Upsert
|
|
66
66
|
|
67
67
|
first_try = true
|
68
68
|
begin
|
69
|
-
create! if connection.in_transaction? && !function_exists?
|
69
|
+
create! if !@assume_function_exists && (connection.in_transaction? && !function_exists?)
|
70
70
|
execute_parameterized(sql, values.map { |v| connection.bind_value v })
|
71
71
|
rescue self.class::ERROR_CLASS => pg_error
|
72
72
|
if pg_error.message =~ /function #{name}.* does not exist/i
|
@@ -85,8 +85,7 @@ class Upsert
|
|
85
85
|
end
|
86
86
|
|
87
87
|
def function_exists?
|
88
|
-
|
89
|
-
@function_exists ||= controller.connection.execute("SELECT count(*)::int AS cnt FROM pg_proc WHERE lower(proname) = lower('#{name}')").first["cnt"].to_i > 0
|
88
|
+
@function_exists ||= controller.connection.execute("SELECT count(*) AS cnt FROM pg_proc WHERE lower(proname) = lower('#{name}')").first["cnt"].to_i > 0
|
90
89
|
end
|
91
90
|
|
92
91
|
# strangely ? can't be used as a placeholder
|
@@ -107,34 +106,93 @@ class Upsert
|
|
107
106
|
end
|
108
107
|
|
109
108
|
def use_pg_native?
|
110
|
-
|
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
|
111
114
|
end
|
112
115
|
|
113
116
|
def server_version
|
114
|
-
@server_version ||=
|
115
|
-
controller.connection.execute("SHOW server_version").first["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
|
116
154
|
end
|
117
155
|
|
118
|
-
def
|
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
|
+
|
119
167
|
execute_parameterized(
|
120
168
|
%{
|
121
|
-
SELECT
|
122
|
-
|
123
|
-
|
124
|
-
|
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
|
125
181
|
},
|
126
|
-
|
182
|
+
table_name_arguments
|
127
183
|
)
|
128
184
|
end
|
129
185
|
|
130
186
|
def pg_native(row)
|
131
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" : ""
|
132
190
|
|
133
191
|
upsert_sql = %{
|
134
192
|
INSERT INTO #{quoted_table_name} (#{quoted_setter_names.join(',')})
|
135
193
|
VALUES (#{insert_bind_placeholders(row).join(', ')})
|
136
194
|
ON CONFLICT(#{quoted_selector_names.join(', ')})
|
137
|
-
DO UPDATE SET
|
195
|
+
DO UPDATE SET #{quoted_setter_names.zip(conflict_bind_placeholders(row)).map { |n, v| "#{n} = #{v}" }.join(', ')}
|
138
196
|
}
|
139
197
|
|
140
198
|
execute_parameterized(upsert_sql, bind_setter_values)
|
@@ -157,13 +215,17 @@ class Upsert
|
|
157
215
|
def insert_bind_placeholders(row)
|
158
216
|
if row.hstore_delete_keys.empty?
|
159
217
|
@insert_bind_placeholders ||= setter_column_definitions.each_with_index.map do |column_definition, i|
|
160
|
-
|
218
|
+
if column_definition.hstore?
|
219
|
+
"CAST($#{i + 1} AS hstore)"
|
220
|
+
else
|
221
|
+
"$#{i + 1}"
|
222
|
+
end
|
161
223
|
end
|
162
224
|
else
|
163
225
|
setter_column_definitions.each_with_index.map do |column_definition, i|
|
164
226
|
idx = i + 1
|
165
227
|
if column_definition.hstore?
|
166
|
-
hstore_delete_function("$#{idx}", row, column_definition)
|
228
|
+
hstore_delete_function("CAST($#{idx} AS hstore)", row, column_definition)
|
167
229
|
else
|
168
230
|
"$#{idx}"
|
169
231
|
end
|
@@ -176,8 +238,8 @@ class Upsert
|
|
176
238
|
@conflict_bind_placeholders ||= setter_column_definitions.each_with_index.map do |column_definition, i|
|
177
239
|
idx = i + 1
|
178
240
|
if column_definition.hstore?
|
179
|
-
"CASE WHEN #{quoted_table_name}.#{column_definition.quoted_name} IS NULL THEN $#{idx} ELSE" \
|
180
|
-
+ " (#{quoted_table_name}.#{column_definition.quoted_name} || $#{idx})" \
|
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))" \
|
181
243
|
+ " END"
|
182
244
|
else
|
183
245
|
"$#{idx}"
|
@@ -188,9 +250,9 @@ class Upsert
|
|
188
250
|
idx = i + 1
|
189
251
|
if column_definition.hstore?
|
190
252
|
"CASE WHEN #{quoted_table_name}.#{column_definition.quoted_name} IS NULL THEN " \
|
191
|
-
+ hstore_delete_function("$#{idx}", row, column_definition) \
|
253
|
+
+ hstore_delete_function("CAST($#{idx} AS hstore)", row, column_definition) \
|
192
254
|
+ " ELSE " \
|
193
|
-
+ hstore_delete_function("(#{quoted_table_name}.#{column_definition.quoted_name} || $#{idx})", row, column_definition) \
|
255
|
+
+ hstore_delete_function("(#{quoted_table_name}.#{column_definition.quoted_name} || CAST($#{idx} AS hstore))", row, column_definition) \
|
194
256
|
+ " END"
|
195
257
|
else
|
196
258
|
"$#{idx}"
|
@@ -2,6 +2,16 @@ class Upsert
|
|
2
2
|
class MergeFunction
|
3
3
|
# @private
|
4
4
|
module Sqlite3
|
5
|
+
def self.included(klass)
|
6
|
+
klass.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def clear(*)
|
11
|
+
# not necessary
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
5
15
|
attr_reader :quoted_setter_names
|
6
16
|
attr_reader :quoted_update_names
|
7
17
|
attr_reader :quoted_selector_names
|
data/lib/upsert/version.rb
CHANGED
data/spec/correctness_spec.rb
CHANGED
@@ -13,7 +13,8 @@ describe Upsert do
|
|
13
13
|
u = Upsert.new($conn, :pets)
|
14
14
|
selector = {:name => 'Jerry', :tag_number => 6}
|
15
15
|
u.row(selector)
|
16
|
-
|
16
|
+
p.reload.tag_number.should == 5
|
17
|
+
next
|
17
18
|
|
18
19
|
# won't change anything because selector is wrong
|
19
20
|
u = Upsert.new($conn, :pets)
|
@@ -100,6 +101,15 @@ describe Upsert do
|
|
100
101
|
lambda { u.row(:name => 'Jerry', :gibberish => 'ba', :gender => 'male') }.should raise_error(/invalid col/i)
|
101
102
|
end
|
102
103
|
|
104
|
+
it "works with a long setter hash" do
|
105
|
+
Upsert.batch($conn, :alphabets) do |batch|
|
106
|
+
10_000.times do |time|
|
107
|
+
setter = Hash[("a".."z").map { |letter| ["the_letter_#{letter}".to_sym, rand(100)] }]
|
108
|
+
selector = Hash[("a".."z").map { |letter| ["the_letter_#{letter}".to_sym, rand(100)] }]
|
109
|
+
batch.row(setter, selector)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
103
113
|
end
|
104
114
|
|
105
115
|
describe "is just as correct as other ways" do
|
@@ -107,8 +117,8 @@ describe Upsert do
|
|
107
117
|
it "is as correct as than new/set/save" do
|
108
118
|
assert_same_result lotsa_records do |records|
|
109
119
|
records.each do |selector, setter|
|
110
|
-
if pet = Pet.where(selector).first
|
111
|
-
pet.update_attributes
|
120
|
+
if (pet = Pet.where(selector).first)
|
121
|
+
pet.update_attributes(setter)
|
112
122
|
else
|
113
123
|
pet = Pet.new
|
114
124
|
selector.each do |k, v|
|
@@ -148,12 +158,15 @@ describe Upsert do
|
|
148
158
|
# end
|
149
159
|
end
|
150
160
|
|
151
|
-
if ENV['DB'] == 'mysql' &&
|
161
|
+
if ENV['DB'] == 'mysql' || (UNIQUE_CONSTRAINT && ENV["DB"] == "postgresql")
|
152
162
|
describe 'compared to activerecord-import' do
|
153
163
|
it "is as correct as faking upserts with activerecord-import" do
|
154
164
|
assert_same_result lotsa_records do |records|
|
155
165
|
columns = nil
|
156
166
|
all_values = []
|
167
|
+
# Reverse because we want to mimic an 'overwrite' of previous values
|
168
|
+
records = records.reverse.uniq { |s, _| s } if ENV['DB'] == "postgresql"
|
169
|
+
|
157
170
|
records.each do |selector, setter|
|
158
171
|
columns ||= (selector.keys + setter.keys).uniq
|
159
172
|
all_values << columns.map do |k|
|
@@ -165,7 +178,9 @@ describe Upsert do
|
|
165
178
|
end
|
166
179
|
end
|
167
180
|
end
|
168
|
-
|
181
|
+
|
182
|
+
conflict_update = ENV['DB'] == "postgresql" ? {conflict_target: records.first.first.keys, columns: columns} : columns
|
183
|
+
Pet.import columns, all_values, :timestamps => false, :on_duplicate_key_update => conflict_update
|
169
184
|
end
|
170
185
|
end
|
171
186
|
end
|
@@ -1,10 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'stringio'
|
3
|
+
require 'upsert/merge_function/postgresql'
|
4
|
+
|
3
5
|
describe Upsert do
|
4
6
|
describe 'database functions' do
|
5
|
-
version = 'postgresql' == ENV['DB'] ?
|
7
|
+
version = 'postgresql' == ENV['DB'] ? Upsert::MergeFunction::Postgresql.extract_version(
|
8
|
+
Pet.connection.select_value("SHOW server_version")
|
9
|
+
) : 0
|
6
10
|
before(:each) {
|
7
|
-
skip "Not using DB functions" if 'postgresql' == ENV['DB'] && UNIQUE_CONSTRAINT && version >=
|
11
|
+
skip "Not using DB functions" if 'postgresql' == ENV['DB'] && UNIQUE_CONSTRAINT && version >= 90500
|
8
12
|
}
|
9
13
|
it "does not re-use merge functions across connections" do
|
10
14
|
begin
|
data/spec/hstore_spec.rb
CHANGED
@@ -3,6 +3,21 @@ require 'spec_helper'
|
|
3
3
|
describe Upsert do
|
4
4
|
describe 'hstore on pg' do
|
5
5
|
require 'pg_hstore'
|
6
|
+
|
7
|
+
let(:deserializer) do
|
8
|
+
klass = PgHstore.dup
|
9
|
+
if RUBY_PLATFORM == "java"
|
10
|
+
# activerecord-jdbc-adapter has native support for hstore
|
11
|
+
klass.class_eval do
|
12
|
+
def self.parse(obj)
|
13
|
+
obj
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
klass
|
19
|
+
end
|
20
|
+
|
6
21
|
Pet.connection.execute 'CREATE EXTENSION IF NOT EXISTS HSTORE'
|
7
22
|
Pet.connection.execute "ALTER TABLE pets ADD COLUMN crazy HSTORE"
|
8
23
|
Pet.connection.execute "ALTER TABLE pets ADD COLUMN cool HSTORE"
|
@@ -19,7 +34,7 @@ describe Upsert do
|
|
19
34
|
EOS
|
20
35
|
upsert.row({:name => 'Uggy'}, :crazy => {:uggy => uggy})
|
21
36
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Uggy'})
|
22
|
-
crazy =
|
37
|
+
crazy = deserializer.parse row['crazy']
|
23
38
|
crazy.should == { 'uggy' => uggy }
|
24
39
|
end
|
25
40
|
|
@@ -30,7 +45,7 @@ EOS
|
|
30
45
|
|
31
46
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
32
47
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
33
|
-
crazy =
|
48
|
+
crazy = deserializer.parse row['crazy']
|
34
49
|
crazy.should == { 'a' => '1' }
|
35
50
|
|
36
51
|
upsert.row({:name => 'Bill'}, :crazy => nil)
|
@@ -39,29 +54,29 @@ EOS
|
|
39
54
|
|
40
55
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
41
56
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
42
|
-
crazy =
|
57
|
+
crazy = deserializer.parse row['crazy']
|
43
58
|
crazy.should == { 'a' => '1' }
|
44
59
|
|
45
60
|
upsert.row({:name => 'Bill'}, :crazy => {:whatdat => 'whodat'})
|
46
61
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
47
|
-
crazy =
|
62
|
+
crazy = deserializer.parse row['crazy']
|
48
63
|
crazy.should == { 'a' => '1', 'whatdat' => 'whodat' }
|
49
64
|
|
50
65
|
upsert.row({:name => 'Bill'}, :crazy => {:whatdat => "D'ONOFRIO"})
|
51
66
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
52
|
-
crazy =
|
67
|
+
crazy = deserializer.parse row['crazy']
|
53
68
|
crazy.should == { 'a' => '1', 'whatdat' => "D'ONOFRIO" }
|
54
69
|
|
55
70
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 2})
|
56
71
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
57
|
-
crazy =
|
72
|
+
crazy = deserializer.parse row['crazy']
|
58
73
|
crazy.should == { 'a' => '2', 'whatdat' => "D'ONOFRIO" }
|
59
74
|
end
|
60
75
|
|
61
76
|
it "can nullify entire hstore" do
|
62
77
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
63
78
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
64
|
-
crazy =
|
79
|
+
crazy = deserializer.parse row['crazy']
|
65
80
|
crazy.should == { 'a' => '1' }
|
66
81
|
|
67
82
|
upsert.row({:name => 'Bill'}, :crazy => nil)
|
@@ -76,42 +91,42 @@ EOS
|
|
76
91
|
|
77
92
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
78
93
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
79
|
-
crazy =
|
94
|
+
crazy = deserializer.parse row['crazy']
|
80
95
|
crazy.should == { 'a' => '1' }
|
81
96
|
|
82
97
|
upsert.row({:name => 'Bill'}, :crazy => {})
|
83
98
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
84
|
-
crazy =
|
99
|
+
crazy = deserializer.parse row['crazy']
|
85
100
|
crazy.should == { 'a' => '1' }
|
86
101
|
|
87
102
|
upsert.row({:name => 'Bill'}, :crazy => {:a => nil})
|
88
103
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
89
|
-
crazy =
|
104
|
+
crazy = deserializer.parse row['crazy']
|
90
105
|
crazy.should == {}
|
91
106
|
|
92
107
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1, :b => 5})
|
93
108
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
94
|
-
crazy =
|
109
|
+
crazy = deserializer.parse row['crazy']
|
95
110
|
crazy.should == { 'a' => '1', 'b' => '5' }
|
96
111
|
|
97
112
|
upsert.row({:name => 'Bill'}, :crazy => {})
|
98
113
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
99
|
-
crazy =
|
114
|
+
crazy = deserializer.parse row['crazy']
|
100
115
|
crazy.should == { 'a' => '1', 'b' => '5' }
|
101
116
|
|
102
117
|
upsert.row({:name => 'Bill'}, :crazy => {:a => nil})
|
103
118
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
104
|
-
crazy =
|
119
|
+
crazy = deserializer.parse row['crazy']
|
105
120
|
crazy.should == { 'b' => '5' }
|
106
121
|
|
107
122
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1, :b => 5})
|
108
123
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
109
|
-
crazy =
|
124
|
+
crazy = deserializer.parse row['crazy']
|
110
125
|
crazy.should == { 'a' => '1', 'b' => '5' }
|
111
126
|
|
112
127
|
upsert.row({:name => 'Bill'}, :crazy => {:a => nil, :b => nil, :c => 12})
|
113
128
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
114
|
-
crazy =
|
129
|
+
crazy = deserializer.parse row['crazy']
|
115
130
|
crazy.should == { 'c' => '12' }
|
116
131
|
end
|
117
132
|
|
@@ -122,111 +137,111 @@ EOS
|
|
122
137
|
|
123
138
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => 1})
|
124
139
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
125
|
-
crazy =
|
140
|
+
crazy = deserializer.parse row['crazy']
|
126
141
|
crazy.should == { 'foo"bar' => '1' }
|
127
142
|
|
128
143
|
upsert.row({:name => 'Bill'}, :crazy => {})
|
129
144
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
130
|
-
crazy =
|
145
|
+
crazy = deserializer.parse row['crazy']
|
131
146
|
crazy.should == { 'foo"bar' => '1' }
|
132
147
|
|
133
148
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => nil})
|
134
149
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
135
|
-
crazy =
|
150
|
+
crazy = deserializer.parse row['crazy']
|
136
151
|
crazy.should == {}
|
137
152
|
|
138
153
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => 1, :b => 5})
|
139
154
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
140
|
-
crazy =
|
155
|
+
crazy = deserializer.parse row['crazy']
|
141
156
|
crazy.should == { 'foo"bar' => '1', 'b' => '5' }
|
142
157
|
|
143
158
|
upsert.row({:name => 'Bill'}, :crazy => {})
|
144
159
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
145
|
-
crazy =
|
160
|
+
crazy = deserializer.parse row['crazy']
|
146
161
|
crazy.should == { 'foo"bar' => '1', 'b' => '5' }
|
147
162
|
|
148
163
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => nil})
|
149
164
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
150
|
-
crazy =
|
165
|
+
crazy = deserializer.parse row['crazy']
|
151
166
|
crazy.should == { 'b' => '5' }
|
152
167
|
|
153
168
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => 1, :b => 5})
|
154
169
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
155
|
-
crazy =
|
170
|
+
crazy = deserializer.parse row['crazy']
|
156
171
|
crazy.should == { 'foo"bar' => '1', 'b' => '5' }
|
157
172
|
|
158
173
|
upsert.row({:name => 'Bill'}, :crazy => {:'foo"bar' => nil, :b => nil, :c => 12})
|
159
174
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
160
|
-
crazy =
|
175
|
+
crazy = deserializer.parse row['crazy']
|
161
176
|
crazy.should == { 'c' => '12' }
|
162
177
|
end
|
163
178
|
|
164
179
|
it "handles multiple hstores" do
|
165
180
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1, :b => 9}, :cool => {:c => 12, :d => 19})
|
166
181
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
167
|
-
crazy =
|
182
|
+
crazy = deserializer.parse row['crazy']
|
168
183
|
crazy.should == { 'a' => '1', 'b' => '9' }
|
169
|
-
cool =
|
184
|
+
cool = deserializer.parse row['cool']
|
170
185
|
cool.should == { 'c' => '12', 'd' => '19' }
|
171
186
|
end
|
172
187
|
|
173
188
|
it "can deletes keys from multiple hstores at once" do
|
174
189
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1}, :cool => {5 => 9})
|
175
190
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
176
|
-
crazy =
|
191
|
+
crazy = deserializer.parse row['crazy']
|
177
192
|
crazy.should == { 'a' => '1' }
|
178
|
-
cool =
|
193
|
+
cool = deserializer.parse row['cool']
|
179
194
|
cool.should == { '5' => '9' }
|
180
195
|
|
181
196
|
# NOOP
|
182
197
|
upsert.row({:name => 'Bill'}, :crazy => {}, :cool => {})
|
183
198
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
184
|
-
crazy =
|
199
|
+
crazy = deserializer.parse row['crazy']
|
185
200
|
crazy.should == { 'a' => '1' }
|
186
|
-
cool =
|
201
|
+
cool = deserializer.parse row['cool']
|
187
202
|
cool.should == { '5' => '9' }
|
188
203
|
|
189
204
|
upsert.row({:name => 'Bill'}, :crazy => {:a => nil}, :cool => {13 => 17})
|
190
205
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
191
|
-
crazy =
|
206
|
+
crazy = deserializer.parse row['crazy']
|
192
207
|
crazy.should == {}
|
193
|
-
cool =
|
208
|
+
cool = deserializer.parse row['cool']
|
194
209
|
cool.should == { '5' => '9', '13' => '17' }
|
195
210
|
|
196
211
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1, :b => 5})
|
197
212
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
198
|
-
crazy =
|
213
|
+
crazy = deserializer.parse row['crazy']
|
199
214
|
crazy.should == { 'a' => '1', 'b' => '5' }
|
200
215
|
|
201
216
|
upsert.row({:name => 'Bill'}, :crazy => {:b => nil}, :cool => {5 => nil})
|
202
217
|
row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
|
203
|
-
crazy =
|
218
|
+
crazy = deserializer.parse row['crazy']
|
204
219
|
crazy.should == {'a' => '1'}
|
205
|
-
cool =
|
220
|
+
cool = deserializer.parse row['cool']
|
206
221
|
cool.should == {'13' => '17' }
|
207
222
|
end
|
208
223
|
|
209
224
|
it "deletes keys whether new or existing record" do
|
210
225
|
upsert.row({:name => 'Bill'}, :crazy => {:z => 1, :x => nil})
|
211
226
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
212
|
-
crazy =
|
227
|
+
crazy = deserializer.parse row['crazy']
|
213
228
|
crazy.should == { 'z' => '1' }
|
214
229
|
|
215
230
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
216
231
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
217
|
-
crazy =
|
232
|
+
crazy = deserializer.parse row['crazy']
|
218
233
|
crazy.should == { 'a' => '1', 'z' => '1' }
|
219
234
|
end
|
220
235
|
|
221
236
|
it "can turn off eager nullify" do
|
222
237
|
upsert.row({:name => 'Bill'}, {:crazy => {:z => 1, :x => nil}}, :eager_nullify => false)
|
223
238
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
224
|
-
crazy =
|
239
|
+
crazy = deserializer.parse row['crazy']
|
225
240
|
crazy.should == { 'z' => '1', 'x' => nil }
|
226
241
|
|
227
242
|
upsert.row({:name => 'Bill'}, :crazy => {:a => 1})
|
228
243
|
row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
|
229
|
-
crazy =
|
244
|
+
crazy = deserializer.parse row['crazy']
|
230
245
|
crazy.should == { 'a' => '1', 'z' => '1', 'x' => nil}
|
231
246
|
end
|
232
247
|
|