em-pg-client-helper 2.0.1 → 2.0.2
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/transaction.rb +85 -27
- data/spec/db_bulk_insert_spec.rb +27 -10
- data/spec/db_transaction_spec.rb +3 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5d0deea932297d54ea2850f086680cd7621f5402
|
4
|
+
data.tar.gz: ffbb8fa419655a46c072799871bda278a815559a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9682d5cc639060a96e73f4e061987400bb1a70335bb74eb4c9f4bc3f391500721705a32e6a0d492bf3826fbb3dceb2728359d7e1a9c5144fa94feb893593ffd
|
7
|
+
data.tar.gz: 9b78c7ab701b04ae9ecb1ab34f0ed8981500c61d9b1a8ced8ca5aabf396ad3db5bd1031ddad8a1ed5b9f6354d755d53a557339684d820a78f39d4a90c24a2972
|
@@ -238,31 +238,54 @@ class PG::EM::Client::Helper::Transaction
|
|
238
238
|
# @since 2.0.0
|
239
239
|
#
|
240
240
|
def bulk_insert(tbl, columns, rows, &blk)
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
241
|
+
columns.map!(&:to_sym)
|
242
|
+
|
243
|
+
EM::Completion.new.tap do |df|
|
244
|
+
@dg.add(df)
|
245
|
+
df.callback(&blk) if blk
|
246
|
+
|
247
|
+
unique_index_columns_for_table(tbl) do |indexes|
|
248
|
+
# Guh hand-hacked SQL is fugly... but what I'm doing is so utterly
|
249
|
+
# niche that Sequel doesn't support it.
|
250
|
+
q_tbl = usdb.literal(tbl.to_sym)
|
251
|
+
q_cols = columns.map { |c| usdb.literal(c) }
|
252
|
+
|
253
|
+
# If there are any unique indexes which the set of columns to
|
254
|
+
# be inserted into *doesn't* completely cover, we need to error
|
255
|
+
# out, because that will cause no rows (or, at most, one row) to
|
256
|
+
# be successfully inserted. In *theory*, a unique index with all-but-one
|
257
|
+
# row inserted *could* work, but that would only work if every value
|
258
|
+
# inserted was different, but quite frankly, I think that's a wicked
|
259
|
+
# corner case I'm not going to go *anywhere* near.
|
260
|
+
#
|
261
|
+
unless indexes.all? { |i| (i - columns).empty? }
|
262
|
+
ex = ArgumentError.new("Unique index columns not covered by data columns")
|
263
|
+
if @autorollback_on_error
|
264
|
+
df.fail(ex)
|
265
|
+
rollback(ex)
|
266
|
+
else
|
267
|
+
df.fail(ex)
|
268
|
+
end
|
269
|
+
else
|
270
|
+
subselect_where = indexes.map do |idx|
|
271
|
+
"(" + idx.map do |c|
|
272
|
+
"src.#{usdb.literal(c)}=dst.#{usdb.literal(c)}"
|
273
|
+
end.join(" AND ") + ")"
|
274
|
+
end.join(" OR ")
|
275
|
+
|
276
|
+
subselect = "SELECT 1 FROM #{q_tbl} AS dst WHERE #{subselect_where}"
|
277
|
+
|
278
|
+
vals = rows.map do |row|
|
279
|
+
"(" + row.map { |v| usdb.literal(v) }.join(", ") + ")"
|
280
|
+
end.join(", ")
|
281
|
+
q = "INSERT INTO #{q_tbl} (SELECT * FROM (VALUES #{vals}) " +
|
282
|
+
"AS src (#{q_cols.join(", ")}) WHERE NOT EXISTS (#{subselect}))"
|
283
|
+
exec(q) do |res|
|
284
|
+
df.succeed(res.cmd_tuples)
|
285
|
+
end
|
245
286
|
end
|
246
|
-
end
|
247
|
-
|
248
|
-
# Guh hand-hacked SQL is fugly... but what I'm doing is so utterly
|
249
|
-
# niche that Sequel doesn't support it.
|
250
|
-
q_tbl = mock_db.literal(tbl.to_sym)
|
251
|
-
q_cols = columns.map { |c| mock_db.literal(c.to_sym) }
|
252
|
-
|
253
|
-
subselect = "SELECT 1 FROM #{q_tbl} AS dst WHERE " +
|
254
|
-
q_cols.map { |c| "src.#{c}=dst.#{c}" }.join(" AND ")
|
255
|
-
|
256
|
-
vals = rows.map do |row|
|
257
|
-
"(" + row.map { |v| mock_db.literal(v) }.join(", ") + ")"
|
258
|
-
end.join(", ")
|
259
|
-
q = "INSERT INTO #{q_tbl} (SELECT * FROM (VALUES #{vals}) " +
|
260
|
-
"AS src (#{q_cols.join(", ")}) WHERE NOT EXISTS (#{subselect}))"
|
261
|
-
exec(q).tap do |df|
|
262
|
-
df.callback do |res|
|
263
|
-
df.succeed(res.cmd_tuples)
|
264
|
-
end
|
265
|
-
df.callback(&blk) if blk
|
287
|
+
end.errback do |ex|
|
288
|
+
df.fail(ex)
|
266
289
|
end
|
267
290
|
end
|
268
291
|
end
|
@@ -293,12 +316,13 @@ class PG::EM::Client::Helper::Transaction
|
|
293
316
|
# specific query finishes.
|
294
317
|
#
|
295
318
|
def exec(sql, values=[], &blk)
|
319
|
+
trace_query(sql, values)
|
320
|
+
|
296
321
|
if @finished
|
297
322
|
raise ClosedError,
|
298
323
|
"Cannot execute a query in a transaction that has been closed"
|
299
324
|
end
|
300
325
|
|
301
|
-
trace_query(sql, values)
|
302
326
|
@conn.exec_defer(sql, values).tap do |df|
|
303
327
|
@dg.add(df)
|
304
328
|
df.callback(&blk) if blk
|
@@ -316,7 +340,41 @@ class PG::EM::Client::Helper::Transaction
|
|
316
340
|
$stderr.puts "#{@conn.inspect}: #{q} #{v.inspect}" if ENV['EM_PG_TXN_TRACE']
|
317
341
|
end
|
318
342
|
|
319
|
-
|
320
|
-
|
343
|
+
# The Utility Sequel Data Base. A mock PgSQL Sequel database that we can
|
344
|
+
# call methods like `#literal` on, for easy quoting.
|
345
|
+
#
|
346
|
+
def usdb
|
347
|
+
@usdb ||= Sequel.connect("mock://postgres")
|
348
|
+
end
|
349
|
+
|
350
|
+
# Find the unique indexes for a table, and yield the columns in each.
|
351
|
+
#
|
352
|
+
# This is used to work out what sets of fields to match on when doing
|
353
|
+
# bulk inserts.
|
354
|
+
#
|
355
|
+
# @yield [Array<Array<Symbol>>] Each element in the yielded array is a
|
356
|
+
# list of the fields which make up a single unique index.
|
357
|
+
#
|
358
|
+
def unique_index_columns_for_table(t)
|
359
|
+
q = "SELECT a.attrelid AS idxid,
|
360
|
+
a.attname AS name,
|
361
|
+
(SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)
|
362
|
+
FROM pg_catalog.pg_attrdef d
|
363
|
+
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef) AS default
|
364
|
+
FROM pg_catalog.pg_attribute AS a
|
365
|
+
JOIN pg_catalog.pg_index AS i ON a.attrelid=i.indexrelid
|
366
|
+
JOIN pg_catalog.pg_class AS c ON c.oid=i.indrelid
|
367
|
+
WHERE c.relname=#{usdb.literal(t.to_s)}
|
368
|
+
AND a.attnum > 0
|
369
|
+
AND NOT a.attisdropped
|
370
|
+
AND i.indisunique"
|
371
|
+
|
372
|
+
exec(q) do |res|
|
373
|
+
yield(res.to_a.each_with_object(Hash.new { |h,k| h[k] = [] }) do |r, h|
|
374
|
+
# Skip auto-incrementing fields; they can take care of themselves
|
375
|
+
next if r["default"] =~ /^nextval/
|
376
|
+
h[r["idxid"]] << r["name"].to_sym
|
377
|
+
end.values)
|
378
|
+
end
|
321
379
|
end
|
322
380
|
end
|
data/spec/db_bulk_insert_spec.rb
CHANGED
@@ -2,26 +2,43 @@ require_relative './spec_helper'
|
|
2
2
|
|
3
3
|
describe "PG::EM::Client::Helper#db_bulk_insert" do
|
4
4
|
let(:mock_conn) { double(PG::EM::Client) }
|
5
|
+
let(:fast_query) do
|
6
|
+
'INSERT INTO "foo" ' +
|
7
|
+
'(SELECT * FROM (VALUES (1, \'x\'), (3, \'y\')) ' +
|
8
|
+
'AS src ("bar", "baz") ' +
|
9
|
+
'WHERE NOT EXISTS ' +
|
10
|
+
'(SELECT 1 FROM "foo" AS dst ' +
|
11
|
+
'WHERE (src."bar"=dst."bar" AND src."baz"=dst."baz")))'
|
12
|
+
end
|
13
|
+
|
14
|
+
def expect_unique_indexes_query(indexes)
|
15
|
+
indexes.map! do |i|
|
16
|
+
idxid = rand(1000000).to_s
|
17
|
+
i.map { |n| {"idxid" => idxid, "name" => n.to_s} }
|
18
|
+
end
|
19
|
+
indexes.flatten!
|
20
|
+
|
21
|
+
expect_query(/^SELECT a\.attrelid/,
|
22
|
+
[], 0.001, :succeed, indexes
|
23
|
+
)
|
24
|
+
end
|
5
25
|
|
6
26
|
it "inserts multiple records" do
|
7
27
|
expect(dbl = double).to receive(:results).with(2)
|
28
|
+
expect(dbl).to_not receive(:errback)
|
8
29
|
|
9
30
|
in_em do
|
10
31
|
expect_query("BEGIN")
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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")
|
32
|
+
expect_unique_indexes_query([[:bar, :baz]])
|
33
|
+
expect_query(fast_query,
|
34
|
+
[], 0.001, :succeed, Struct.new(:cmd_tuples).new(2)
|
35
|
+
)
|
36
|
+
expect_query("COMMIT")
|
20
37
|
|
21
38
|
db_bulk_insert(mock_conn, "foo", [:bar, :baz], [[1, "x"], [3, "y"]]) do |count|
|
22
39
|
dbl.results(count)
|
23
40
|
EM.stop
|
24
|
-
end
|
41
|
+
end.errback { dbl.errback; EM.stop }
|
25
42
|
end
|
26
43
|
end
|
27
44
|
end
|
data/spec/db_transaction_spec.rb
CHANGED
@@ -35,15 +35,15 @@ describe "PG::EM::Client::Helper#db_transaction" do
|
|
35
35
|
|
36
36
|
it "fails the transaction if COMMIT fails" do
|
37
37
|
dbl = double
|
38
|
-
expect(dbl).to receive(:
|
39
|
-
expect(dbl).to_not receive(:
|
38
|
+
expect(dbl).to receive(:errback)
|
39
|
+
expect(dbl).to_not receive(:callback)
|
40
40
|
|
41
41
|
in_em do
|
42
42
|
expect_query("BEGIN")
|
43
43
|
expect_query_failure("COMMIT")
|
44
44
|
in_transaction do |txn|
|
45
45
|
txn.commit
|
46
|
-
end.callback { dbl.
|
46
|
+
end.callback { dbl.callback }.errback { dbl.errback }
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
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: 2.0.
|
4
|
+
version: 2.0.2
|
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-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: em-pg-client
|