em-pg-client-helper 2.0.1 → 2.0.2
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/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
|