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 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