em-pg-client-helper 1.3.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/em-pg-client-helper.rb +54 -2
- data/lib/em-pg-client-helper/deferrable_group.rb +13 -6
- data/lib/em-pg-client-helper/transaction.rb +157 -19
- data/spec/db_bulk_insert_spec.rb +27 -0
- data/spec/db_transaction_savepoint_spec.rb +118 -0
- data/spec/db_transaction_spec.rb +31 -0
- data/spec/txn_helper.rb +10 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 54910da339d3b8be844ddddb6b373dbec7ca04b5
|
4
|
+
data.tar.gz: efb8474eb8d13f6020116b4923dfea5f4793059c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c39f44d77f9238e333d22fd9ed3a6970f46c45a783d3414f733baa61a912479524032c962fdacd0e085e56f38c0615d3dba3e30fcbe66198168ab657a1c113ba
|
7
|
+
data.tar.gz: 012405e9483038a357080c994b20ce3ce0793379e557b28f04331a34d70a07a2871afeadc98f1c6303d5d3397073fb61db0ff7b4a4c8826f8fff2cb7aee82055
|
data/lib/em-pg-client-helper.rb
CHANGED
@@ -76,10 +76,10 @@ module PG::EM::Client::Helper
|
|
76
76
|
sqls = sqldb.sqls
|
77
77
|
|
78
78
|
if sqls.empty?
|
79
|
-
sqls = [ret.sql] rescue
|
79
|
+
sqls = [ret.sql] rescue ret
|
80
80
|
end
|
81
81
|
|
82
|
-
if sqls.empty?
|
82
|
+
if sqls.nil? or sqls.empty?
|
83
83
|
raise PG::EM::Client::Helper::BadSequelError,
|
84
84
|
"Your block did not generate an SQL statement"
|
85
85
|
end
|
@@ -195,6 +195,58 @@ module PG::EM::Client::Helper
|
|
195
195
|
db.exec_defer(*insert_sql(tbl, params))
|
196
196
|
end
|
197
197
|
|
198
|
+
# Efficiently perform a "bulk" insert of multiple rows.
|
199
|
+
#
|
200
|
+
# When you have a large quantity of data to insert into a table, you don't
|
201
|
+
# want to do it one row at a time -- that's *really* inefficient. On the
|
202
|
+
# other hand, if you do one giant multi-row insert statement, the insert
|
203
|
+
# will fail if *any* of the rows causes a constraint failure. What to do?
|
204
|
+
#
|
205
|
+
# Well, here's our answer: try to insert all the records at once. If that
|
206
|
+
# fails with a constraint violation, then split the set of records in half
|
207
|
+
# and try to bulk insert each of those halves. Recurse in this fashion until
|
208
|
+
# you only have one record to insert.
|
209
|
+
#
|
210
|
+
# @param db [PG::EM::Client, PG::EM::ConnectionPool] the connection against
|
211
|
+
# which the insert wil be run.
|
212
|
+
#
|
213
|
+
# @param tbl [#to_sym] the name of the table into which you wish to insert
|
214
|
+
# your data.
|
215
|
+
#
|
216
|
+
# @param columns [Array<#to_sym>] the columns into which each record of data
|
217
|
+
# will be inserted.
|
218
|
+
#
|
219
|
+
# @param rows [Array<Array<Object>>] the values to insert. Each entry in
|
220
|
+
# the outermost array is a row of data; the elements of each of these inner
|
221
|
+
# arrays corresponds to the column in the same position in the `columns`
|
222
|
+
# array. **NOTE**: we don't do any checking to make sure you're giving
|
223
|
+
# us the correct list of values for each row. Thus, if you give us a
|
224
|
+
# row array that has too few, or too many, entries, the database will puke.
|
225
|
+
#
|
226
|
+
# @return [EM::Deferrable] the deferrable in which the query is being called;
|
227
|
+
# once the bulk insert completes successfully, the deferrable will succeed
|
228
|
+
# with the number of rows that were successfully inserted. If the insert
|
229
|
+
# could not be completed, the deferrable will fail (`#errback`) with the
|
230
|
+
# exception.
|
231
|
+
#
|
232
|
+
# @since 2.0.0
|
233
|
+
#
|
234
|
+
def db_bulk_insert(db, tbl, columns, rows, &blk)
|
235
|
+
EM::Completion.new.tap do |df|
|
236
|
+
df.callback(&blk) if blk
|
237
|
+
|
238
|
+
db_transaction(db) do |txn|
|
239
|
+
txn.bulk_insert(tbl, columns, rows) do |count|
|
240
|
+
txn.commit do
|
241
|
+
df.succeed(count)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end.errback do |ex|
|
245
|
+
df.fail(ex)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
198
250
|
# @!macro upsert_params
|
199
251
|
#
|
200
252
|
# @param tbl [#to_s] The name of the table on which to operate.
|
@@ -7,6 +7,13 @@
|
|
7
7
|
class PG::EM::Client::Helper::DeferrableGroup
|
8
8
|
include ::EventMachine::Deferrable
|
9
9
|
|
10
|
+
# Raised when an attempt is made to add a deferrable to a group on
|
11
|
+
# which {#close} has already been called.
|
12
|
+
#
|
13
|
+
# @since 2.0.0
|
14
|
+
#
|
15
|
+
class ClosedError < StandardError; end
|
16
|
+
|
10
17
|
# Create a new deferrable group.
|
11
18
|
#
|
12
19
|
def initialize
|
@@ -23,15 +30,15 @@ class PG::EM::Client::Helper::DeferrableGroup
|
|
23
30
|
#
|
24
31
|
# @return [EM::Deferrable] the same deferrable.
|
25
32
|
#
|
26
|
-
# @raise [
|
27
|
-
#
|
28
|
-
#
|
29
|
-
# to add.
|
33
|
+
# @raise [PG::EM::Client::Helper::DeferrableGroup::ClosedError] if you
|
34
|
+
# attempt to add a deferrable after the group has been closed (that is,
|
35
|
+
# the `#close` method has been called), indicating that the deferrable
|
36
|
+
# group doesn't have any more deferrables to add.
|
30
37
|
#
|
31
38
|
def add(df)
|
32
39
|
if @closed
|
33
|
-
raise
|
34
|
-
"This deferrable group is closed
|
40
|
+
raise ClosedError,
|
41
|
+
"This deferrable group is closed"
|
35
42
|
end
|
36
43
|
|
37
44
|
@outstanding << df
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
1
3
|
# Represents a database transaction, and contains all of the methods which
|
2
4
|
# can be used to execute queries within the transaction connection.
|
3
5
|
#
|
@@ -5,6 +7,13 @@ class PG::EM::Client::Helper::Transaction
|
|
5
7
|
include ::PG::EM::Client::Helper
|
6
8
|
include ::EventMachine::Deferrable
|
7
9
|
|
10
|
+
# Raised when you attempt to execute a query in a transaction which has
|
11
|
+
# been finished (either by COMMIT or ROLLBACK).
|
12
|
+
#
|
13
|
+
# @since 2.0.0
|
14
|
+
#
|
15
|
+
class ClosedError < StandardError; end
|
16
|
+
|
8
17
|
# Create a new transaction. You shouldn't have to call this yourself;
|
9
18
|
# `db_transaction` should create one and pass it to your block.
|
10
19
|
#
|
@@ -21,12 +30,12 @@ class PG::EM::Client::Helper::Transaction
|
|
21
30
|
# @raise [ArgumentError] If an unknown isolation level is specified.
|
22
31
|
#
|
23
32
|
def initialize(conn, opts = {}, &blk)
|
24
|
-
@conn
|
25
|
-
@opts
|
26
|
-
|
27
|
-
|
28
|
-
@
|
29
|
-
@
|
33
|
+
@conn = conn
|
34
|
+
@opts = opts
|
35
|
+
@finished = nil
|
36
|
+
@retryable = opts[:retry]
|
37
|
+
@autorollback_on_error = true
|
38
|
+
@savepoint_stack = []
|
30
39
|
|
31
40
|
DeferrableGroup.new do |dg|
|
32
41
|
@dg = dg
|
@@ -59,7 +68,7 @@ class PG::EM::Client::Helper::Transaction
|
|
59
68
|
end
|
60
69
|
end
|
61
70
|
end.callback do
|
62
|
-
rollback(RuntimeError.new("txn.commit was not called")) unless @
|
71
|
+
rollback(RuntimeError.new("txn.commit was not called")) unless @finished
|
63
72
|
self.succeed
|
64
73
|
end.errback do |ex|
|
65
74
|
if @retryable and [PG::TRSerializationFailure].include?(ex.class)
|
@@ -81,16 +90,16 @@ class PG::EM::Client::Helper::Transaction
|
|
81
90
|
# exception will be raised.
|
82
91
|
#
|
83
92
|
def commit
|
84
|
-
|
93
|
+
unless @finished
|
85
94
|
trace_query("COMMIT")
|
86
95
|
@conn.exec_defer("COMMIT", []).tap do |df|
|
87
96
|
@dg.add(df)
|
88
97
|
end.callback do
|
89
|
-
@
|
98
|
+
@finished = true
|
90
99
|
@dg.close
|
91
|
-
|
92
|
-
|
93
|
-
@
|
100
|
+
yield if block_given?
|
101
|
+
end.errback do
|
102
|
+
@finished = true
|
94
103
|
@dg.close
|
95
104
|
end
|
96
105
|
end
|
@@ -102,15 +111,81 @@ class PG::EM::Client::Helper::Transaction
|
|
102
111
|
# event of a database error or other exception.
|
103
112
|
#
|
104
113
|
def rollback(ex)
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
114
|
+
unless @finished
|
115
|
+
if @savepoint_stack.empty?
|
116
|
+
exec("ROLLBACK") do
|
117
|
+
@finished = true
|
118
|
+
@dg.fail(ex)
|
119
|
+
@dg.close
|
120
|
+
end
|
121
|
+
else
|
122
|
+
sp = @savepoint_stack.pop
|
123
|
+
exec("ROLLBACK TO \"#{sp[:savepoint]}\"")
|
124
|
+
sp[:deferrable].fail(ex)
|
109
125
|
@dg.close
|
126
|
+
@dg = sp[:parent_deferrable_group]
|
110
127
|
end
|
111
128
|
end
|
112
129
|
end
|
113
130
|
|
131
|
+
# Manage the "rollback on the failure of a single query" behaviour.
|
132
|
+
#
|
133
|
+
# The default behaviour of a transaction, when a query fails, is for
|
134
|
+
# the transaction to automatically be rolled back and the rest of the
|
135
|
+
# statements to not be executed. In **ALMOST** every case, this is the
|
136
|
+
# correct behaviour. However, there are some corner cases in which you
|
137
|
+
# want to be able to avoid this behaviour, and will manually react to
|
138
|
+
# the transaction failure in some way. In that case, you can set this
|
139
|
+
# to `false` and the transaction will not automatically fail.
|
140
|
+
#
|
141
|
+
# Given that pretty much the only thing you can do when a query fails,
|
142
|
+
# other than abort the transaction, is to rollback to a savepoint, you
|
143
|
+
# might want to look at {#savepoint} before you try using this.
|
144
|
+
#
|
145
|
+
# @since 2.0.0
|
146
|
+
#
|
147
|
+
attr_accessor :autorollback_on_error
|
148
|
+
|
149
|
+
# Setup a "savepoint" within the transaction.
|
150
|
+
#
|
151
|
+
# A savepoint is, as the name suggests, kinda like a "saved game", in an
|
152
|
+
# SQL transaction. If a query fails within a transaction, normally all
|
153
|
+
# you can do is rollback and abort the entire transaction. Savepoints
|
154
|
+
# give you another option: roll back to the savepoint, and try again.
|
155
|
+
#
|
156
|
+
# So, that's what this method does. Inside of the block passed to
|
157
|
+
# `#savepoint`, if any query fails, instead of rolling back the entire
|
158
|
+
# transaction, we instead only rollback to the savepoint, and execution
|
159
|
+
# continues by executing the `errback` callbacks defined on the savepoint
|
160
|
+
# deferrable.
|
161
|
+
#
|
162
|
+
# @return [EM::Deferrable]
|
163
|
+
#
|
164
|
+
# @since 2.0.0
|
165
|
+
#
|
166
|
+
def savepoint(&blk)
|
167
|
+
savepoint = SecureRandom.uuid
|
168
|
+
parent_dg = @dg
|
169
|
+
DeferrableGroup.new do |dg|
|
170
|
+
@dg = dg
|
171
|
+
|
172
|
+
dg.callback do
|
173
|
+
@dg = parent_dg
|
174
|
+
@dg.close
|
175
|
+
end
|
176
|
+
|
177
|
+
exec("SAVEPOINT \"#{savepoint}\"").tap do |df|
|
178
|
+
@savepoint_stack << { :savepoint => savepoint,
|
179
|
+
:deferrable => df,
|
180
|
+
:parent_deferrable_group => parent_dg
|
181
|
+
}
|
182
|
+
|
183
|
+
df.callback(&blk) if blk
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
|
114
189
|
# Generate SQL statements via Sequel, and run the result against the
|
115
190
|
# database. Very chic.
|
116
191
|
#
|
@@ -131,6 +206,67 @@ class PG::EM::Client::Helper::Transaction
|
|
131
206
|
exec(*insert_sql(tbl, params), &blk)
|
132
207
|
end
|
133
208
|
|
209
|
+
# Efficiently perform a "bulk" insert of multiple rows.
|
210
|
+
#
|
211
|
+
# When you have a large quantity of data to insert into a table, you don't
|
212
|
+
# want to do it one row at a time -- that's *really* inefficient. On the
|
213
|
+
# other hand, if you do one giant multi-row insert statement, the insert
|
214
|
+
# will fail if *any* of the rows causes a constraint failure. What to do?
|
215
|
+
#
|
216
|
+
# Well, here's our answer: try to insert all the records at once. If that
|
217
|
+
# fails with a constraint violation, then split the set of records in half
|
218
|
+
# and try to bulk insert each of those halves. Recurse in this fashion until
|
219
|
+
# you only have one record to insert.
|
220
|
+
#
|
221
|
+
# @param tbl [#to_sym] the name of the table into which you wish to insert
|
222
|
+
# your data.
|
223
|
+
#
|
224
|
+
# @param columns [Array<#to_sym>] the columns into which each record of data
|
225
|
+
# will be inserted.
|
226
|
+
#
|
227
|
+
# @param rows [Array<Array<Object>>] the values to insert. Each entry in
|
228
|
+
# the outermost array is a row of data; the elements of each of these inner
|
229
|
+
# arrays corresponds to the column in the same position in the `columns`
|
230
|
+
# array. **NOTE**: we don't do any checking to make sure you're giving
|
231
|
+
# us the correct list of values for each row. Thus, if you give us a
|
232
|
+
# row array that has too few, or too many, entries, the database will puke.
|
233
|
+
#
|
234
|
+
# @yield [Integer] Once the insert has completed, the number of rows that
|
235
|
+
# were successfully inserted (that may be less than `rows.length` if
|
236
|
+
# there were any constraint failures) will be yielded to the block.
|
237
|
+
#
|
238
|
+
# @since 2.0.0
|
239
|
+
#
|
240
|
+
def bulk_insert(tbl, columns, rows, &blk)
|
241
|
+
db = Sequel.connect("mock://postgres")
|
242
|
+
|
243
|
+
# Guh hand-hacked SQL is fugly... but what I'm doing is so utterly
|
244
|
+
# niche that Sequel doesn't support it.
|
245
|
+
q_tbl = db.literal(tbl.to_sym)
|
246
|
+
q_cols = columns.map { |c| db.literal(c.to_sym) }
|
247
|
+
|
248
|
+
subselect = "SELECT 1 FROM #{q_tbl} AS dst WHERE " +
|
249
|
+
q_cols.map { |c| "src.#{c}=dst.#{c}" }.join(" AND ")
|
250
|
+
|
251
|
+
total_rows_inserted = 0
|
252
|
+
DeferrableGroup.new.tap do |dg|
|
253
|
+
rows.each_slice(100) do |slice|
|
254
|
+
vals = slice.map do |row|
|
255
|
+
"(" + row.map { |v| db.literal(v) }.join(", ") + ")"
|
256
|
+
end.join(", ")
|
257
|
+
q = "INSERT INTO #{q_tbl} (SELECT * FROM (VALUES #{vals}) " +
|
258
|
+
"AS src (#{q_cols.join(", ")}) WHERE NOT EXISTS (#{subselect}))"
|
259
|
+
df = exec(q) do |res|
|
260
|
+
total_rows_inserted += res.cmd_tuples
|
261
|
+
end
|
262
|
+
dg.add(df)
|
263
|
+
end
|
264
|
+
dg.callback { dg.succeed(total_rows_inserted) }
|
265
|
+
dg.callback(&blk) if blk
|
266
|
+
dg.close
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
134
270
|
# Run an upsert inside a transaction.
|
135
271
|
#
|
136
272
|
# @see {PG::EM::Client::Helper#upsert_sql} for all the parameters.
|
@@ -157,8 +293,8 @@ class PG::EM::Client::Helper::Transaction
|
|
157
293
|
# specific query finishes.
|
158
294
|
#
|
159
295
|
def exec(sql, values=[], &blk)
|
160
|
-
|
161
|
-
raise
|
296
|
+
if @finished
|
297
|
+
raise ClosedError,
|
162
298
|
"Cannot execute a query in a transaction that has been closed"
|
163
299
|
end
|
164
300
|
|
@@ -167,11 +303,13 @@ class PG::EM::Client::Helper::Transaction
|
|
167
303
|
@dg.add(df)
|
168
304
|
df.callback(&blk) if blk
|
169
305
|
end.errback do |ex|
|
170
|
-
rollback(ex)
|
306
|
+
rollback(ex) if @autorollback_on_error
|
171
307
|
end
|
172
308
|
end
|
173
309
|
alias_method :exec_defer, :exec
|
174
310
|
|
311
|
+
private
|
312
|
+
|
175
313
|
# Trace queries as they happen, if `ENV['EM_PG_TXN_TRACE']` is set.
|
176
314
|
#
|
177
315
|
def trace_query(q, v=nil)
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe "PG::EM::Client::Helper#db_bulk_insert" do
|
4
|
+
let(:mock_conn) { double(PG::EM::Client) }
|
5
|
+
|
6
|
+
it "inserts multiple records" do
|
7
|
+
expect(dbl = double).to receive(:results).with(2)
|
8
|
+
|
9
|
+
in_em do
|
10
|
+
expect_query("BEGIN")
|
11
|
+
expect_query('INSERT INTO "foo" ' +
|
12
|
+
'(SELECT * FROM (VALUES (1, \'x\'), (3, \'y\')) ' +
|
13
|
+
'AS src ("bar", "baz") ' +
|
14
|
+
'WHERE NOT EXISTS ' +
|
15
|
+
'(SELECT 1 FROM "foo" AS dst ' +
|
16
|
+
'WHERE src."bar"=dst."bar" AND src."baz"=dst."baz"))',
|
17
|
+
[], 0.001, :succeed, Struct.new(:cmd_tuples).new(2)
|
18
|
+
)
|
19
|
+
expect_query("COMMIT")
|
20
|
+
|
21
|
+
db_bulk_insert(mock_conn, "foo", [:bar, :baz], [[1, "x"], [3, "y"]]) do |count|
|
22
|
+
dbl.results(count)
|
23
|
+
EM.stop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
require_relative './spec_helper'
|
2
|
+
|
3
|
+
describe "PG::EM::Client::Helper::Transaction#savepoint" do
|
4
|
+
let(:mock_conn) { double(PG::EM::Client) }
|
5
|
+
|
6
|
+
it "executes through the savepoint in normal operation" do
|
7
|
+
in_em do
|
8
|
+
expect(SecureRandom).to receive(:uuid).and_return("faff")
|
9
|
+
expect_query("BEGIN")
|
10
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wombat"])
|
11
|
+
expect_query('SAVEPOINT "faff"')
|
12
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
13
|
+
expect_query("COMMIT")
|
14
|
+
in_transaction do |txn|
|
15
|
+
txn.insert("foo", :bar => 'wombat') do
|
16
|
+
txn.savepoint do
|
17
|
+
txn.insert("foo", :bar => 'baz') do
|
18
|
+
txn.commit
|
19
|
+
end
|
20
|
+
end.errback do
|
21
|
+
txn.rollback # Just to show this *doesn't* happen
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "rolls back to the savepoint after a failed INSERT" do
|
29
|
+
in_em do
|
30
|
+
expect(SecureRandom).to receive(:uuid).and_return("faff")
|
31
|
+
expect_query("BEGIN")
|
32
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wombat"])
|
33
|
+
expect_query('SAVEPOINT "faff"')
|
34
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
35
|
+
expect_query('ROLLBACK TO "faff"')
|
36
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wibble"])
|
37
|
+
expect_query("COMMIT")
|
38
|
+
in_transaction do |txn|
|
39
|
+
txn.insert("foo", :bar => 'wombat') do
|
40
|
+
txn.savepoint do
|
41
|
+
txn.insert("foo", :bar => 'baz') do
|
42
|
+
txn.rollback # Just to show this *doesn't* happen
|
43
|
+
end
|
44
|
+
end.errback do
|
45
|
+
txn.insert("foo", :bar => 'wibble') do
|
46
|
+
txn.commit
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "issues ROLLBACK if query fails outside of savepoint" do
|
55
|
+
in_em do
|
56
|
+
expect(SecureRandom).to receive(:uuid).and_return("faff")
|
57
|
+
expect_query("BEGIN")
|
58
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wombat"])
|
59
|
+
expect_query('SAVEPOINT "faff"')
|
60
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
61
|
+
# I'd like to be able to verify that the savepoint IDs are the same
|
62
|
+
# in both queries, but I'm not sure how. Cross fingers!
|
63
|
+
expect_query('ROLLBACK TO "faff"')
|
64
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["wibble"])
|
65
|
+
expect_query("ROLLBACK")
|
66
|
+
in_transaction do |txn|
|
67
|
+
txn.insert("foo", :bar => 'wombat') do
|
68
|
+
txn.savepoint do
|
69
|
+
txn.insert("foo", :bar => 'baz') do
|
70
|
+
txn.rollback # Just to show this *doesn't* happen
|
71
|
+
end
|
72
|
+
end.errback do
|
73
|
+
txn.insert("foo", :bar => 'wibble') do
|
74
|
+
txn.commit
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
it "handles nested SAVEPOINTs correctly" do
|
83
|
+
in_em do
|
84
|
+
expect(SecureRandom).to receive(:uuid).and_return("faff1")
|
85
|
+
expect(SecureRandom).to receive(:uuid).and_return("faff2")
|
86
|
+
expect_query("BEGIN")
|
87
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wombat"])
|
88
|
+
expect_query('SAVEPOINT "faff1"')
|
89
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
90
|
+
# I'd like to be able to verify that the savepoint IDs are the same
|
91
|
+
# in both queries, but I'm not sure how. Cross fingers!
|
92
|
+
expect_query('ROLLBACK TO "faff1"')
|
93
|
+
|
94
|
+
expect_query('SAVEPOINT "faff2"')
|
95
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["wibble"])
|
96
|
+
expect_query('ROLLBACK TO "faff2"')
|
97
|
+
expect_query("COMMIT")
|
98
|
+
|
99
|
+
in_transaction do |txn|
|
100
|
+
txn.insert("foo", :bar => 'wombat') do
|
101
|
+
txn.savepoint do
|
102
|
+
txn.insert("foo", :bar => 'baz') do
|
103
|
+
txn.rollback # Just to show this *doesn't* happen
|
104
|
+
end
|
105
|
+
end.errback do
|
106
|
+
txn.savepoint do
|
107
|
+
txn.insert("foo", :bar => 'wibble') do
|
108
|
+
txn.rollback # Another "shouldn't happen"
|
109
|
+
end.errback do
|
110
|
+
txn.commit
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
data/spec/db_transaction_spec.rb
CHANGED
@@ -33,6 +33,20 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
it "fails the transaction if COMMIT fails" do
|
37
|
+
dbl = double
|
38
|
+
expect(dbl).to receive(:foo)
|
39
|
+
expect(dbl).to_not receive(:bar)
|
40
|
+
|
41
|
+
in_em do
|
42
|
+
expect_query("BEGIN")
|
43
|
+
expect_query_failure("COMMIT")
|
44
|
+
in_transaction do |txn|
|
45
|
+
txn.commit
|
46
|
+
end.callback { dbl.bar }.errback { dbl.foo }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
36
50
|
it "runs a simple INSERT correctly" do
|
37
51
|
in_em do
|
38
52
|
expect_query("BEGIN")
|
@@ -219,4 +233,21 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
219
233
|
end
|
220
234
|
end
|
221
235
|
end
|
236
|
+
|
237
|
+
it "doesn't rollback back after a failed INSERT with autorollback = false" do
|
238
|
+
in_em do
|
239
|
+
expect_query("BEGIN")
|
240
|
+
expect_query_failure('INSERT INTO "foo" ("bar") VALUES ($1)', ["baz"])
|
241
|
+
expect_query('INSERT INTO "foo" ("bar") VALUES ($1)', ["wombat"])
|
242
|
+
expect_query("COMMIT")
|
243
|
+
in_transaction do |txn|
|
244
|
+
txn.autorollback_on_error = false
|
245
|
+
txn.insert("foo", :bar => 'baz').errback do
|
246
|
+
txn.insert("foo", :bar => 'wombat') do
|
247
|
+
txn.commit
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
222
253
|
end
|
data/spec/txn_helper.rb
CHANGED
@@ -23,9 +23,16 @@ module TxnHelper
|
|
23
23
|
end
|
24
24
|
|
25
25
|
def in_em
|
26
|
-
|
27
|
-
|
28
|
-
|
26
|
+
begin
|
27
|
+
Timeout.timeout(0.5) do
|
28
|
+
EM.run do
|
29
|
+
yield
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue Timeout::Error
|
33
|
+
EM.stop if EM.reactor_running?
|
34
|
+
raise RuntimeError,
|
35
|
+
"EM test time exceeded"
|
29
36
|
end
|
30
37
|
end
|
31
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: em-pg-client-helper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matt Palmer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: em-pg-client
|
@@ -230,7 +230,9 @@ files:
|
|
230
230
|
- lib/em-pg-client-helper.rb
|
231
231
|
- lib/em-pg-client-helper/deferrable_group.rb
|
232
232
|
- lib/em-pg-client-helper/transaction.rb
|
233
|
+
- spec/db_bulk_insert_spec.rb
|
233
234
|
- spec/db_insert_spec.rb
|
235
|
+
- spec/db_transaction_savepoint_spec.rb
|
234
236
|
- spec/db_transaction_sequel_spec.rb
|
235
237
|
- spec/db_transaction_spec.rb
|
236
238
|
- spec/db_transaction_upsert_spec.rb
|