em-pg-client-helper 1.3.0 → 2.0.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.
- 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
|