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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f805f782a78651db5a93dbcba7516e3e1b0abe4f
4
- data.tar.gz: 72132229cb096de84eb4c823e2d49b1d5691cc40
3
+ metadata.gz: 5d0deea932297d54ea2850f086680cd7621f5402
4
+ data.tar.gz: ffbb8fa419655a46c072799871bda278a815559a
5
5
  SHA512:
6
- metadata.gz: 3a8390c30559d4986d5a9704ea4c118b5948cb87811c6208e6399bd874ee2a5979c1b3690b78684be69c3f6e77b28fa93f06c2bb6e9de6cffe52b0756b1b0bd5
7
- data.tar.gz: b6efc7278ca5a06be0930cfa7a0a5125e5345f6215d27fa724e2d59fd2897700cf0e2bd679fd1d8eebc7fb30287722b5f285179941996361354757747df1de9a
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
- if rows.length > 1000
242
- bulk_insert(tbl, columns, rows[0..999]) do |count1|
243
- bulk_insert(tbl, columns, rows[1000..-1]) do |count2|
244
- blk.call(count1 + count2)
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
- else
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
- def mock_db
320
- @mock_db ||= Sequel.connect("mock://postgres")
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
@@ -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
- 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")
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
@@ -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(:foo)
39
- expect(dbl).to_not receive(:bar)
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.bar }.errback { dbl.foo }
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.1
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-15 00:00:00.000000000 Z
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