em-pg-client-helper 1.0.0 → 1.1.0
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.
- 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
|