em-pg-client-helper 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/em-pg-client-helper.rb +100 -0
- data/lib/em-pg-client-helper/transaction.rb +13 -0
- data/spec/db_transaction_spec.rb +0 -30
- data/spec/db_transaction_upsert_spec.rb +87 -0
- data/spec/db_upsert_spec.rb +58 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/txn_helper.rb +31 -0
- data/spec/upsert_sql_spec.rb +32 -0
- metadata +8 -4
data/lib/em-pg-client-helper.rb
CHANGED
@@ -47,6 +47,106 @@ module PG::EM::Client::Helper
|
|
47
47
|
db.exec_defer(*insert_sql(tbl, params))
|
48
48
|
end
|
49
49
|
|
50
|
+
# @macro upsert_params
|
51
|
+
#
|
52
|
+
# @param tbl [#to_s] The name of the table on which to operate.
|
53
|
+
#
|
54
|
+
# @param key [#to_s, Array<#to_s>] A field (or list of fields) which
|
55
|
+
# are the set of values that uniquely identify the record to be
|
56
|
+
# updated, if it exists. You only need to specify the field names
|
57
|
+
# here, as the values which will be used in the query will be taken
|
58
|
+
# from the `data`.
|
59
|
+
#
|
60
|
+
# @param data [Hash<#to_s, Object>] The fields and values to insert into
|
61
|
+
# the database, or to set in the existing record.
|
62
|
+
#
|
63
|
+
# @raise [ArgumentError] if a field is specified in `key` but which
|
64
|
+
# does not exist in `data`.
|
65
|
+
#
|
66
|
+
#
|
67
|
+
# An "upsert" is a kind of crazy hybrid "update if the record exists,
|
68
|
+
# insert it if it doesn't" query. It isn't part of the SQL standard,
|
69
|
+
# but it is such a common idiom that we're keen to support it.
|
70
|
+
#
|
71
|
+
# The trick is that it's actually two queries in one. We try to do an
|
72
|
+
# `UPDATE` first, and if that doesn't actually update anything, then we
|
73
|
+
# try an `INSERT`. Since it is two separate queries, though, there is still
|
74
|
+
# a small chance that the query will fail with a `PG::UniqueViolation`, so
|
75
|
+
# your code must handle that.
|
76
|
+
#
|
77
|
+
# @!macro upsert_params
|
78
|
+
#
|
79
|
+
# @return [Array<String, Array<Object>>] A two-element array, the first
|
80
|
+
# of which is a string containing the literal SQL to be executed, while
|
81
|
+
# the second element is an array containing the values, in order
|
82
|
+
# corresponding to the placeholders in the SQL.
|
83
|
+
#
|
84
|
+
def upsert_sql(tbl, key, data)
|
85
|
+
tbl = quote_identifier(tbl)
|
86
|
+
insert_keys = data.keys.map { |k| quote_identifier(k.to_s) }
|
87
|
+
unique_keys = (key.is_a?(Array) ? key : [key])
|
88
|
+
unique_keys.map! { |k| quote_identifier(k.to_s) }
|
89
|
+
update_keys = insert_keys - unique_keys
|
90
|
+
|
91
|
+
unless (bad_keys = unique_keys - insert_keys).empty?
|
92
|
+
raise ArgumentError,
|
93
|
+
"These field(s) were mentioned in the key list, but were not in the data set: #{bad_keys.inspect}"
|
94
|
+
end
|
95
|
+
|
96
|
+
values = data.values
|
97
|
+
# field-to-placeholder mapping
|
98
|
+
i = 0
|
99
|
+
fp_map = Hash[insert_keys.map { |k| i += 1; [k, "$#{i}"] }]
|
100
|
+
|
101
|
+
update_values = update_keys.map { |k| "#{k}=#{fp_map[k]}" }.join(',')
|
102
|
+
select_values = unique_keys.map { |k| "#{k}=#{fp_map[k]}" }.join(' AND ')
|
103
|
+
update_query = "UPDATE #{tbl} SET #{update_values} WHERE #{select_values} RETURNING *"
|
104
|
+
|
105
|
+
insert_query = "INSERT INTO #{tbl} (#{fp_map.keys.join(',')}) SELECT #{fp_map.values.join(',')}"
|
106
|
+
|
107
|
+
["WITH upsert AS (#{update_query}) #{insert_query} WHERE NOT EXISTS (SELECT * FROM upsert)",
|
108
|
+
data.values
|
109
|
+
]
|
110
|
+
end
|
111
|
+
|
112
|
+
# Run an upsert query.
|
113
|
+
#
|
114
|
+
# @see #upsert_sql
|
115
|
+
#
|
116
|
+
# Apply an upsert (update-or-insert) against a given database connection or
|
117
|
+
# connection pool, handling the (rarely needed) unique violation that can
|
118
|
+
# result.
|
119
|
+
#
|
120
|
+
# @param db [PG::EM::Client, PG::EM::ConnectionPool] the connection
|
121
|
+
# against which all database operations will be run.
|
122
|
+
#
|
123
|
+
# @!macro upsert_params
|
124
|
+
#
|
125
|
+
# @return [EM::Deferrable] the deferrable in which the query is being
|
126
|
+
# called; this means you should attach the code to run after the query
|
127
|
+
# completes with `#callback`, and you can attach an error handler with
|
128
|
+
# `#errback` if you like.
|
129
|
+
#
|
130
|
+
def db_upsert(db, tbl, key, data)
|
131
|
+
q = upsert_sql(tbl, key, data)
|
132
|
+
|
133
|
+
::EM::DefaultDeferrable.new.tap do |df|
|
134
|
+
db.exec_defer(*q).callback do
|
135
|
+
df.succeed
|
136
|
+
end.errback do |ex|
|
137
|
+
if ex.is_a?(PG::UniqueViolation)
|
138
|
+
db.exec_defer(*q).callback do
|
139
|
+
df.succeed
|
140
|
+
end.errback do |ex|
|
141
|
+
df.fail(ex)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
df.fail(ex)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
50
150
|
# Execute code in a transaction.
|
51
151
|
#
|
52
152
|
# Calling this method opens up a transaction (by executing `BEGIN`), and
|
@@ -117,6 +117,19 @@ class PG::EM::Client::Helper::Transaction
|
|
117
117
|
exec(*insert_sql(tbl, params), &blk)
|
118
118
|
end
|
119
119
|
|
120
|
+
# Run an upsert inside a transaction.
|
121
|
+
#
|
122
|
+
# @see {PG::EM::Client::Helper#upsert_sql} for all the parameters.
|
123
|
+
#
|
124
|
+
# @return [EM::Deferrable]
|
125
|
+
def upsert(*args, &blk)
|
126
|
+
db_upsert(@conn, *args).tap do |df|
|
127
|
+
df.callback(&blk) if block_given?
|
128
|
+
end.errback do |ex|
|
129
|
+
rollback(ex)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
120
133
|
# Execute an arbitrary block of SQL in `sql` within the transaction.
|
121
134
|
# If you need to pass dynamic values to the query, those should be
|
122
135
|
# given in `values`, and referenced in `sql` as `$1`, `$2`, etc. The
|
data/spec/db_transaction_spec.rb
CHANGED
@@ -3,36 +3,6 @@ require_relative './spec_helper'
|
|
3
3
|
describe "PG::EM::Client::Helper#db_transaction" do
|
4
4
|
let(:mock_conn) { double(PG::EM::Client) }
|
5
5
|
|
6
|
-
def expect_query_failure(q, args=[], err=nil, exec_time = 0.001)
|
7
|
-
err ||= RuntimeError.new("Dummy failure")
|
8
|
-
expect_query(q, args, exec_time, :fail, err)
|
9
|
-
end
|
10
|
-
|
11
|
-
def expect_query(q, args=[], exec_time = 0.001, disposition = :succeed, *disp_opts)
|
12
|
-
df = EM::DefaultDeferrable.new
|
13
|
-
|
14
|
-
expect(mock_conn)
|
15
|
-
.to receive(:exec_defer)
|
16
|
-
.with(*[q, args].compact)
|
17
|
-
.and_return(df)
|
18
|
-
.ordered
|
19
|
-
|
20
|
-
EM.add_timer(exec_time) do
|
21
|
-
df.__send__(disposition, *disp_opts)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def in_transaction(*args, &blk)
|
26
|
-
db_transaction(mock_conn, *args, &blk).callback { EM.stop }.errback { EM.stop }
|
27
|
-
end
|
28
|
-
|
29
|
-
def in_em
|
30
|
-
EM.run do
|
31
|
-
EM.add_timer(0.5) { EM.stop; raise "test timeout" }
|
32
|
-
yield
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
6
|
it "runs a BEGIN/COMMIT cycle by default" do
|
37
7
|
in_em do
|
38
8
|
expect_query("BEGIN")
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe "PG::EM::Client::Helper#db_transaction#upsert" do
|
4
|
+
let(:mock_conn) { double(PG::EM::Client) }
|
5
|
+
let(:mock_query) do
|
6
|
+
[
|
7
|
+
'WITH upsert AS (UPDATE "foo" SET "bar"=$1 WHERE "wombat"=$2 RETURNING *) ' +
|
8
|
+
'INSERT INTO "foo" ("bar","wombat") SELECT $1,$2 ' +
|
9
|
+
'WHERE NOT EXISTS (SELECT * FROM upsert)',
|
10
|
+
["baz", 42]
|
11
|
+
]
|
12
|
+
end
|
13
|
+
let(:mock_query_opts) do
|
14
|
+
["foo", :wombat, { :bar => "baz", :wombat => 42 }]
|
15
|
+
end
|
16
|
+
|
17
|
+
it "runs a simple UPSERT correctly" do
|
18
|
+
in_em do
|
19
|
+
expect_query("BEGIN")
|
20
|
+
expect_query(*mock_query)
|
21
|
+
expect_query("COMMIT")
|
22
|
+
in_transaction do |txn|
|
23
|
+
txn.upsert(*mock_query_opts) do
|
24
|
+
txn.commit
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "rolls back after a failed attempt" do
|
31
|
+
in_em do
|
32
|
+
expect_query("BEGIN")
|
33
|
+
expect_query_failure(*mock_query)
|
34
|
+
expect_query("ROLLBACK")
|
35
|
+
in_transaction do |txn|
|
36
|
+
txn.upsert(*mock_query_opts) do
|
37
|
+
txn.commit
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it "tries again if the first failure was a PG::UniqueViolation" do
|
44
|
+
in_em do
|
45
|
+
expect_query("BEGIN")
|
46
|
+
expect_query_failure(*mock_query, PG::UniqueViolation.new("OMFG"))
|
47
|
+
expect_query(*mock_query)
|
48
|
+
expect_query("COMMIT")
|
49
|
+
|
50
|
+
in_transaction do |txn|
|
51
|
+
txn.upsert(*mock_query_opts) do
|
52
|
+
txn.commit
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "explodes if the second time fails" do
|
59
|
+
in_em do
|
60
|
+
expect_query("BEGIN")
|
61
|
+
expect_query_failure(*mock_query, PG::UniqueViolation.new("OMFG"))
|
62
|
+
expect_query_failure(*mock_query)
|
63
|
+
expect_query("ROLLBACK")
|
64
|
+
|
65
|
+
in_transaction do |txn|
|
66
|
+
txn.upsert(*mock_query_opts) do
|
67
|
+
txn.commit
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "explodes if the second failure was a PG::UniqueViolation" do
|
74
|
+
in_em do
|
75
|
+
expect_query("BEGIN")
|
76
|
+
expect_query_failure(*mock_query, PG::UniqueViolation.new("OMFG"))
|
77
|
+
expect_query_failure(*mock_query, PG::UniqueViolation.new("OMFG"))
|
78
|
+
expect_query("ROLLBACK")
|
79
|
+
|
80
|
+
in_transaction do |txn|
|
81
|
+
txn.upsert(*mock_query_opts) do
|
82
|
+
txn.commit
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe "PG::EM::Client::Helper#db_upsert" do
|
4
|
+
let(:mock_query) do
|
5
|
+
[
|
6
|
+
'WITH upsert AS (UPDATE "foo" SET "bar"=$1 WHERE "wombat"=$2 RETURNING *) ' +
|
7
|
+
'INSERT INTO "foo" ("bar","wombat") SELECT $1,$2 ' +
|
8
|
+
'WHERE NOT EXISTS (SELECT * FROM upsert)',
|
9
|
+
["baz", 42]
|
10
|
+
]
|
11
|
+
end
|
12
|
+
|
13
|
+
it "works first time" do
|
14
|
+
expect(db = double).
|
15
|
+
to receive(:exec_defer).
|
16
|
+
with(*mock_query).
|
17
|
+
and_return(::EM::DefaultDeferrable.new.tap { |df| df.succeed })
|
18
|
+
|
19
|
+
df = db_upsert(db, "foo", :wombat, :bar => "baz", :wombat => 42)
|
20
|
+
expect(df.instance_variable_get(:@deferred_status)).to eq(:succeeded)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "works on retry" do
|
24
|
+
expect(db = double).
|
25
|
+
to receive(:exec_defer).
|
26
|
+
with(*mock_query).
|
27
|
+
ordered.
|
28
|
+
and_return(
|
29
|
+
::EM::DefaultDeferrable.new.tap do |df|
|
30
|
+
df.fail(PG::UniqueViolation.new("OMFG"))
|
31
|
+
end
|
32
|
+
)
|
33
|
+
|
34
|
+
expect(db).
|
35
|
+
to receive(:exec_defer).
|
36
|
+
with(*mock_query).
|
37
|
+
ordered.
|
38
|
+
and_return(::EM::DefaultDeferrable.new.tap { |df| df.succeed })
|
39
|
+
|
40
|
+
df = db_upsert(db, "foo", :wombat, :bar => "baz", :wombat => 42)
|
41
|
+
expect(df.instance_variable_get(:@deferred_status)).to eq(:succeeded)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "fails on a different error" do
|
45
|
+
expect(db = double).
|
46
|
+
to receive(:exec_defer).
|
47
|
+
with(*mock_query).
|
48
|
+
ordered.
|
49
|
+
and_return(
|
50
|
+
::EM::DefaultDeferrable.new.tap do |df|
|
51
|
+
df.fail(PG::UndefinedTable.new("OMFG"))
|
52
|
+
end
|
53
|
+
)
|
54
|
+
|
55
|
+
df = db_upsert(db, "foo", :wombat, :bar => "baz", :wombat => 42)
|
56
|
+
expect(df.instance_variable_get(:@deferred_status)).to eq(:failed)
|
57
|
+
end
|
58
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -5,6 +5,7 @@ Spork.prefork do
|
|
5
5
|
Bundler.setup(:default, :development, :test)
|
6
6
|
require 'rspec/core'
|
7
7
|
require 'rspec/mocks'
|
8
|
+
require 'txn_helper'
|
8
9
|
|
9
10
|
require 'pry'
|
10
11
|
|
@@ -15,6 +16,8 @@ Spork.prefork do
|
|
15
16
|
config.expect_with :rspec do |c|
|
16
17
|
c.syntax = :expect
|
17
18
|
end
|
19
|
+
|
20
|
+
config.include TxnHelper
|
18
21
|
end
|
19
22
|
end
|
20
23
|
|
data/spec/txn_helper.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module TxnHelper
|
2
|
+
def expect_query_failure(q, args=[], err=nil, exec_time = 0.001)
|
3
|
+
err ||= RuntimeError.new("A mock exec_defer failure")
|
4
|
+
expect_query(q, args, exec_time, :fail, err)
|
5
|
+
end
|
6
|
+
|
7
|
+
def expect_query(q, args=[], exec_time = 0.001, disposition = :succeed, *disp_opts)
|
8
|
+
df = EM::DefaultDeferrable.new
|
9
|
+
|
10
|
+
expect(mock_conn)
|
11
|
+
.to receive(:exec_defer)
|
12
|
+
.with(*[q, args].compact)
|
13
|
+
.and_return(df)
|
14
|
+
.ordered
|
15
|
+
|
16
|
+
EM.add_timer(exec_time) do
|
17
|
+
df.__send__(disposition, *disp_opts)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def in_transaction(*args, &blk)
|
22
|
+
db_transaction(mock_conn, *args, &blk).callback { EM.stop }.errback { EM.stop }
|
23
|
+
end
|
24
|
+
|
25
|
+
def in_em
|
26
|
+
EM.run do
|
27
|
+
EM.add_timer(0.5) { EM.stop; raise "test timeout" }
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe "PG::EM::Client::Helper#upsert_sql" do
|
4
|
+
it "assembles a simple query correctly" do
|
5
|
+
expect(upsert_sql("foo", :wombat, :bar => "baz", :wombat => 42)).
|
6
|
+
to eq([
|
7
|
+
'WITH upsert AS (UPDATE "foo" SET "bar"=$1 WHERE "wombat"=$2 RETURNING *) ' +
|
8
|
+
'INSERT INTO "foo" ("bar","wombat") SELECT $1,$2 ' +
|
9
|
+
'WHERE NOT EXISTS (SELECT * FROM upsert)',
|
10
|
+
["baz", 42]
|
11
|
+
])
|
12
|
+
end
|
13
|
+
|
14
|
+
it "assembles a more complex query correctly" do
|
15
|
+
expect(
|
16
|
+
upsert_sql(
|
17
|
+
"user_store_data",
|
18
|
+
[:id, :platform, :profile_type],
|
19
|
+
:id => 42,
|
20
|
+
:platform => 'wooden',
|
21
|
+
:profile_type => 'xyzzy',
|
22
|
+
:data => "ohai!"
|
23
|
+
)
|
24
|
+
).to eq([
|
25
|
+
'WITH upsert AS (UPDATE "user_store_data" SET "data"=$4 ' +
|
26
|
+
'WHERE "id"=$1 AND "platform"=$2 AND "profile_type"=$3 RETURNING *) ' +
|
27
|
+
'INSERT INTO "user_store_data" ("id","platform","profile_type","data") ' +
|
28
|
+
'SELECT $1,$2,$3,$4 WHERE NOT EXISTS (SELECT * FROM upsert)',
|
29
|
+
[42, "wooden", "xyzzy", "ohai!"]
|
30
|
+
])
|
31
|
+
end
|
32
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-pg-client-helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-01-
|
12
|
+
date: 2015-01-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: git-version-bump
|
@@ -245,8 +245,12 @@ files:
|
|
245
245
|
- lib/em-pg-client-helper/transaction.rb
|
246
246
|
- spec/db_insert_spec.rb
|
247
247
|
- spec/db_transaction_spec.rb
|
248
|
+
- spec/db_transaction_upsert_spec.rb
|
249
|
+
- spec/db_upsert_spec.rb
|
248
250
|
- spec/insert_sql_spec.rb
|
249
251
|
- spec/spec_helper.rb
|
252
|
+
- spec/txn_helper.rb
|
253
|
+
- spec/upsert_sql_spec.rb
|
250
254
|
homepage: http://github.com/mpalmer/em-pg-client-helper
|
251
255
|
licenses: []
|
252
256
|
post_install_message:
|
@@ -261,7 +265,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
261
265
|
version: '0'
|
262
266
|
segments:
|
263
267
|
- 0
|
264
|
-
hash:
|
268
|
+
hash: 3057886313740167473
|
265
269
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
266
270
|
none: false
|
267
271
|
requirements:
|
@@ -270,7 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
270
274
|
version: '0'
|
271
275
|
segments:
|
272
276
|
- 0
|
273
|
-
hash:
|
277
|
+
hash: 3057886313740167473
|
274
278
|
requirements: []
|
275
279
|
rubyforge_project:
|
276
280
|
rubygems_version: 1.8.23
|