pgtk 0.32.4 → 0.32.5

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
  SHA256:
3
- metadata.gz: 72cdeed11d9faa8c9097c01256603b2ce77a507b936c983ab623b9e1d6e7d364
4
- data.tar.gz: ab60b5670e87009e1eb47983bd8d729eb403d78a9c6a20c82ea14d70d4a7d18f
3
+ metadata.gz: b1997e1fa9261d1881e5df4495a43723ea737ad754806bc9908a986c4e782fa6
4
+ data.tar.gz: 51b31ffb42b6eb9d2ea625c82be4664384bc190d2c1187ab642cb7cbeaaeb9ab
5
5
  SHA512:
6
- metadata.gz: 691ea0c8b5ca6002b72ab4b66487b3dbf489feaedbc8e80cc772fe69cfc1ad966fa836e13298b561cadb38c75290ed8a533a389751a7d6b0454da23b87afcb74
7
- data.tar.gz: 8a04b05ad839b935ac3f8e5362bb957eb64e8e70e1b963c5713a7c7755f8184e6c1e8c806a4d03467bba80e3ce5af0cf9c1a1a50c4d73f11e79d46dc9569fc2f
6
+ metadata.gz: f6245814e1907f27948dd369e8ff00cf584e759c04c7b22befac1c4da3de828ef6192f8d3510fe6760b04cdbab871045a7963ed54c00cd1d64b982b2ccfb483d
7
+ data.tar.gz: 5f97bf03bf00fc4e4da554b1807896474d4221748ebacc3cbda22da56e6d55e3067dd5e277082c7a4184aabe4617df9ece91e6dfaaec718f2530f7acc4410664
data/Gemfile.lock CHANGED
@@ -69,7 +69,7 @@ GEM
69
69
  nokogiri (1.19.3-x86_64-linux-musl)
70
70
  racc (~> 1.4)
71
71
  os (1.1.4)
72
- parallel (2.0.1)
72
+ parallel (2.1.0)
73
73
  parser (3.3.11.1)
74
74
  ast (~> 2.4.1)
75
75
  racc
@@ -81,7 +81,7 @@ GEM
81
81
  pg (1.6.3-x86_64-linux)
82
82
  pg (1.6.3-x86_64-linux-musl)
83
83
  prism (1.9.0)
84
- qbash (0.8.3)
84
+ qbash (0.8.4)
85
85
  backtrace (> 0)
86
86
  elapsed (> 0)
87
87
  loog (> 0)
@@ -108,7 +108,7 @@ GEM
108
108
  rubocop-ast (1.49.1)
109
109
  parser (>= 3.3.7.2)
110
110
  prism (~> 1.7)
111
- rubocop-elegant (0.0.20)
111
+ rubocop-elegant (0.5.0)
112
112
  lint_roller (~> 1.1)
113
113
  rubocop (~> 1.75)
114
114
  rubocop-minitest (0.39.1)
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../pgtk'
7
+
8
+ class Pgtk::Impatient; end
9
+
10
+ # Raised by Pgtk::Impatient#exec when the query takes longer than the
11
+ # configured timeout. The deadline is enforced server-side via
12
+ # +SET LOCAL statement_timeout+, so the underlying +PG::QueryCanceled+
13
+ # is translated into this error for the caller.
14
+ #
15
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
16
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
17
+ # License:: MIT
18
+ class Pgtk::Impatient::TooSlow < StandardError; end
@@ -58,9 +58,6 @@ require_relative '../pgtk'
58
58
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
59
59
  # License:: MIT
60
60
  class Pgtk::Impatient
61
- # If timed out
62
- class TooSlow < StandardError; end
63
-
64
61
  # Constructor.
65
62
  #
66
63
  # @param [Pgtk::Pool] pool The pool to decorate
@@ -118,14 +115,16 @@ class Pgtk::Impatient
118
115
  t.exec(sql, *args)
119
116
  end
120
117
  rescue PG::QueryCanceled
121
- raise(TooSlow, [
122
- 'SQL query',
123
- ("with #{args.count} argument#{'s' if args.count > 1}" unless args.empty?),
124
- 'was terminated after',
125
- start.ago,
126
- 'of waiting:',
127
- sql.ellipsized(50).inspect
128
- ].compact.join(' '))
118
+ raise(
119
+ TooSlow, [
120
+ 'SQL query',
121
+ ("with #{args.count} argument#{'s' if args.count > 1}" unless args.empty?),
122
+ 'was terminated after',
123
+ start.ago,
124
+ 'of waiting:',
125
+ sql.ellipsized(50).inspect
126
+ ].compact.join(' ')
127
+ )
129
128
  end
130
129
  end
131
130
 
@@ -146,3 +145,5 @@ class Pgtk::Impatient
146
145
  end
147
146
  end
148
147
  end
148
+
149
+ require_relative 'impatient/too_slow'
@@ -127,14 +127,15 @@ class Pgtk::LiquibaseTask < Rake::TaskLib
127
127
  password = yml['pgsql']['password']
128
128
  host = yml.dig('pgsql', 'host')
129
129
  Dir.chdir(File.dirname(@schema)) do
130
- out =
130
+ File.write(
131
+ @schema,
131
132
  if (local && @docker != :always) || @docker == :never
132
133
  pgdump(yml, host, password)
133
134
  else
134
135
  host = donce_host if OS.mac? && ['localhost', '127.0.0.1'].include?(host)
135
136
  dockerdump(yml, host, password)
136
137
  end
137
- File.write(@schema, out)
138
+ )
138
139
  end
139
140
  end
140
141
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../pgtk'
7
+
8
+ class Pgtk::LiquicheckTask; end
9
+
10
+ # Internal error raised by Pgtk::LiquicheckTask validation helpers and
11
+ # captured per-file by the +on+ helper to accumulate readable diagnostics.
12
+ #
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
15
+ # License:: MIT
16
+ class Pgtk::LiquicheckTask::MustError < StandardError
17
+ end
@@ -110,8 +110,6 @@ class Pgtk::LiquicheckTask < Rake::TaskLib
110
110
  def confirm(prop, regex, msg)
111
111
  raise(MustError, msg) unless prop.match?(regex)
112
112
  end
113
-
114
- class MustError < StandardError
115
- end
116
- private_constant :MustError
117
113
  end
114
+
115
+ require_relative 'liquicheck_task/must_error'
@@ -51,18 +51,18 @@ class Pgtk::PgsqlTask < Rake::TaskLib
51
51
 
52
52
  def run
53
53
  local = detect(:local)
54
- docker = detect(:docker)
55
- preflight(local, docker)
54
+ preflight(local, detect(:docker))
56
55
  home = File.expand_path(@dir)
57
56
  FileUtils.rm_rf(home) if @fresh
58
57
  raise(ArgumentError, "Directory/file #{home} is present, use fresh=true") if File.exist?(home)
59
- stdout = @quiet ? nil : $stdout
60
58
  port = acquire
61
- place = launch(local, home, stdout, port)
62
- save(port)
63
- return if @quiet
64
- puts("PostgreSQL has been started #{place}, port #{port}")
65
- puts("YAML config saved to #{@yaml}")
59
+ launch(local, home, @quiet ? nil : $stdout, port).then do |place|
60
+ save(port)
61
+ unless @quiet
62
+ puts("PostgreSQL has been started #{place}, port #{port}")
63
+ puts("YAML config saved to #{@yaml}")
64
+ end
65
+ end
66
66
  end
67
67
 
68
68
  def preflight(local, docker)
@@ -87,11 +87,9 @@ class Pgtk::PgsqlTask < Rake::TaskLib
87
87
 
88
88
  def launch(local, home, stdout, port)
89
89
  if (local && @docker != :always) || @docker == :never
90
- pid = localize(home, stdout, port)
91
- "in process ##{pid}"
90
+ "in process ##{localize(home, stdout, port)}"
92
91
  else
93
- container = dockerize(home, stdout, port)
94
- "in container #{container}"
92
+ "in container #{dockerize(home, stdout, port)}"
95
93
  end
96
94
  end
97
95
 
@@ -122,21 +120,19 @@ class Pgtk::PgsqlTask < Rake::TaskLib
122
120
 
123
121
  def dockerize(home, stdout, port)
124
122
  FileUtils.mkdir_p(home)
125
- out =
126
- qbash(
127
- 'docker',
128
- 'run',
129
- "--publish #{Shellwords.escape("#{port}:5432")}",
130
- "-e POSTGRES_USER=#{Shellwords.escape(@user)}",
131
- "-e POSTGRES_PASSWORD=#{Shellwords.escape(@password)}",
132
- "-e POSTGRES_DB=#{Shellwords.escape(@dbname)}",
133
- '--detach',
134
- '--rm',
135
- 'postgres:18.1',
136
- @config.map { |k, v| "-c #{Shellwords.escape("#{k}=#{v}")}" },
137
- stdout:
138
- )
139
- container = out.scan(/[a-f0-9]+\Z/).first
123
+ container = qbash(
124
+ 'docker',
125
+ 'run',
126
+ "--publish #{Shellwords.escape("#{port}:5432")}",
127
+ "-e POSTGRES_USER=#{Shellwords.escape(@user)}",
128
+ "-e POSTGRES_PASSWORD=#{Shellwords.escape(@password)}",
129
+ "-e POSTGRES_DB=#{Shellwords.escape(@dbname)}",
130
+ '--detach',
131
+ '--rm',
132
+ 'postgres:18.1',
133
+ @config.map { |k, v| "-c #{Shellwords.escape("#{k}=#{v}")}" },
134
+ stdout:
135
+ ).scan(/[a-f0-9]+\Z/).first
140
136
  File.write(File.join(home, 'docker-container'), container)
141
137
  at_exit do
142
138
  if qbash(
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../pgtk'
7
+
8
+ class Pgtk::Pool; end
9
+
10
+ # Raised when no connection becomes available from the pool within
11
+ # the configured timeout. Indicates that all connections are currently
12
+ # taken by other threads and none was returned in time.
13
+ #
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
16
+ # License:: MIT
17
+ class Pgtk::Pool::Busy < StandardError; end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../pgtk'
7
+ require_relative 'busy'
8
+
9
+ class Pgtk::Pool; end
10
+
11
+ # Thread-safe queue implementation that supports iteration.
12
+ # Unlike Ruby's Queue class, this implementation allows safe iteration
13
+ # over all elements while maintaining thread safety for concurrent access.
14
+ #
15
+ # This class is used internally by Pool to store database connections
16
+ # and provide the ability to iterate over them for inspection purposes.
17
+ #
18
+ # The queue is bounded by size. When an item is taken out, it remains in
19
+ # the internal array but is marked as "taken". When returned, it's placed
20
+ # back in its original slot and marked as available.
21
+ #
22
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
23
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
24
+ # License:: MIT
25
+ class Pgtk::Pool::IterableQueue
26
+ def initialize(size, timeout)
27
+ @size = size
28
+ @timeout = timeout
29
+ @items = []
30
+ @taken = []
31
+ @free = []
32
+ @mutex = Mutex.new
33
+ @condition = ConditionVariable.new
34
+ end
35
+
36
+ def push(item)
37
+ @mutex.synchronize do
38
+ if @items.size < @size
39
+ @items << item
40
+ @taken << false
41
+ @free << (@items.size - 1)
42
+ else
43
+ index = @items.index(item)
44
+ if index.nil?
45
+ index = @taken.index(true)
46
+ raise(StandardError, 'No taken slot found') if index.nil?
47
+ @items[index] = item
48
+ end
49
+ @taken[index] = false
50
+ @free << index
51
+ end
52
+ @condition.signal
53
+ end
54
+ end
55
+
56
+ def pop
57
+ @mutex.synchronize do
58
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
59
+ while @free.empty?
60
+ remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
+ if remaining <= 0
62
+ raise(Pgtk::Pool::Busy, "No free connection appeared in the pool after #{@timeout}s of waiting")
63
+ end
64
+ @condition.wait(@mutex, remaining)
65
+ end
66
+ index = @free.shift
67
+ @taken[index] = true
68
+ @items[index]
69
+ end
70
+ end
71
+
72
+ def size
73
+ @mutex.synchronize do
74
+ @items.size
75
+ end
76
+ end
77
+
78
+ def map(&)
79
+ @mutex.synchronize do
80
+ @items.map(&)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'tago'
7
+ require_relative '../../pgtk'
8
+
9
+ class Pgtk::Pool; end
10
+
11
+ # A temporary class to execute a single SQL request.
12
+ #
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
15
+ # License:: MIT
16
+ class Pgtk::Pool::Txn
17
+ def initialize(conn, log)
18
+ @conn = conn
19
+ @log = log
20
+ end
21
+
22
+ # Exec a single parameterized command.
23
+ # @param [String] query The SQL query with params inside (possibly)
24
+ # @param [Array] args List of arguments
25
+ # @param [Integer] result Should be 0 for text results, 1 for binary
26
+ # @yield [Hash] Rows
27
+ def exec(query, args = [], result = 0)
28
+ start = Time.now
29
+ sql = query.is_a?(Array) ? query.join(' ') : query
30
+ @conn.instance_variable_set(:@pgtk_last_query, sql)
31
+ @conn.instance_variable_set(:@pgtk_started_at, start)
32
+ begin
33
+ out =
34
+ if args.empty?
35
+ @conn.exec(sql) do |res|
36
+ if block_given?
37
+ yield(res)
38
+ else
39
+ res.each.to_a
40
+ end
41
+ end
42
+ else
43
+ @conn.exec_params(sql, args, result) do |res|
44
+ if block_given?
45
+ yield(res)
46
+ else
47
+ res.each.to_a
48
+ end
49
+ end
50
+ end
51
+ rescue StandardError => e
52
+ @log.error("#{sql} -> #{e.message}")
53
+ raise(e)
54
+ end
55
+ if (Time.now - start) < 1
56
+ @log.debug("#{sql} >> #{start.ago} / #{@conn.object_id}")
57
+ else
58
+ @log.info("#{sql} >> #{start.ago}")
59
+ end
60
+ out
61
+ end
62
+ end
data/lib/pgtk/pool.rb CHANGED
@@ -50,11 +50,6 @@ require_relative 'wire'
50
50
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
51
51
  # License:: MIT
52
52
  class Pgtk::Pool
53
- # Raised when no connection becomes available from the pool within
54
- # the configured timeout. Indicates that all connections are currently
55
- # taken by other threads and none was returned in time.
56
- class Busy < StandardError; end
57
-
58
53
  # Constructor.
59
54
  #
60
55
  # The +idle+ option guards against the cold-slot SSL desync that bites
@@ -198,9 +193,7 @@ class Pgtk::Pool
198
193
  t = Txn.new(c, @log)
199
194
  t.exec('START TRANSACTION')
200
195
  begin
201
- r = yield(t)
202
- t.exec('COMMIT')
203
- r
196
+ yield(t).tap { t.exec('COMMIT') }
204
197
  ensure
205
198
  if c.transaction_status != PG::Constants::PQTRANS_IDLE
206
199
  begin
@@ -213,124 +206,6 @@ class Pgtk::Pool
213
206
  end
214
207
  end
215
208
 
216
- # Thread-safe queue implementation that supports iteration.
217
- # Unlike Ruby's Queue class, this implementation allows safe iteration
218
- # over all elements while maintaining thread safety for concurrent access.
219
- #
220
- # This class is used internally by Pool to store database connections
221
- # and provide the ability to iterate over them for inspection purposes.
222
- #
223
- # The queue is bounded by size. When an item is taken out, it remains in
224
- # the internal array but is marked as "taken". When returned, it's placed
225
- # back in its original slot and marked as available.
226
- class IterableQueue
227
- def initialize(size, timeout)
228
- @size = size
229
- @timeout = timeout
230
- @items = []
231
- @taken = []
232
- @free = []
233
- @mutex = Mutex.new
234
- @condition = ConditionVariable.new
235
- end
236
-
237
- def push(item)
238
- @mutex.synchronize do
239
- if @items.size < @size
240
- @items << item
241
- @taken << false
242
- @free << (@items.size - 1)
243
- else
244
- index = @items.index(item)
245
- if index.nil?
246
- index = @taken.index(true)
247
- raise(StandardError, 'No taken slot found') if index.nil?
248
- @items[index] = item
249
- end
250
- @taken[index] = false
251
- @free << index
252
- end
253
- @condition.signal
254
- end
255
- end
256
-
257
- def pop
258
- @mutex.synchronize do
259
- deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
260
- while @free.empty?
261
- remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
262
- raise(Busy, "No free connection appeared in the pool after #{@timeout}s of waiting") if remaining <= 0
263
- @condition.wait(@mutex, remaining)
264
- end
265
- index = @free.shift
266
- @taken[index] = true
267
- @items[index]
268
- end
269
- end
270
-
271
- def size
272
- @mutex.synchronize do
273
- @items.size
274
- end
275
- end
276
-
277
- def map(&)
278
- @mutex.synchronize do
279
- @items.map(&)
280
- end
281
- end
282
- end
283
-
284
- # A temporary class to execute a single SQL request.
285
- class Txn
286
- def initialize(conn, log)
287
- @conn = conn
288
- @log = log
289
- end
290
-
291
- # Exec a single parameterized command.
292
- # @param [String] query The SQL query with params inside (possibly)
293
- # @param [Array] args List of arguments
294
- # @param [Integer] result Should be 0 for text results, 1 for binary
295
- # @yield [Hash] Rows
296
- def exec(query, args = [], result = 0)
297
- start = Time.now
298
- sql = query.is_a?(Array) ? query.join(' ') : query
299
- @conn.instance_variable_set(:@pgtk_last_query, sql)
300
- @conn.instance_variable_set(:@pgtk_started_at, start)
301
- begin
302
- out =
303
- if args.empty?
304
- @conn.exec(sql) do |res|
305
- if block_given?
306
- yield(res)
307
- else
308
- res.each.to_a
309
- end
310
- end
311
- else
312
- @conn.exec_params(sql, args, result) do |res|
313
- if block_given?
314
- yield(res)
315
- else
316
- res.each.to_a
317
- end
318
- end
319
- end
320
- rescue StandardError => e
321
- @log.error("#{sql} -> #{e.message}")
322
- raise(e)
323
- end
324
- lag = Time.now - start
325
- if lag < 1
326
- @log.debug("#{sql} >> #{start.ago} / #{@conn.object_id}")
327
- else
328
- @log.info("#{sql} >> #{start.ago}")
329
- end
330
- out
331
- end
332
- end
333
-
334
209
  private
335
210
 
336
211
  def connect
@@ -371,9 +246,9 @@ class Pgtk::Pool
371
246
  end
372
247
 
373
248
  def stale(conn)
374
- return nil if @idle.nil?
249
+ return if @idle.nil?
375
250
  last = conn.instance_variable_get(:@pgtk_last_used)
376
- return nil if last.nil? || Time.now - last < @idle
251
+ return if last.nil? || Time.now - last < @idle
377
252
  begin
378
253
  conn.exec('SELECT 1')
379
254
  nil
@@ -383,19 +258,23 @@ class Pgtk::Pool
383
258
  end
384
259
 
385
260
  def info(conn)
386
- pipelines = { PG::Constants::PQ_PIPELINE_ON => 'ON', PG::Constants::PQ_PIPELINE_OFF => 'OFF',
387
- PG::Constants::PQ_PIPELINE_ABORTED => 'ABORTED' }
388
- statuses = { PG::Constants::CONNECTION_OK => 'OK', PG::Constants::CONNECTION_BAD => 'BAD' }
389
- transactions = { PG::Constants::PQTRANS_IDLE => 'IDLE', PG::Constants::PQTRANS_ACTIVE => 'ACTIVE',
390
- PG::Constants::PQTRANS_INTRANS => 'INTRANS', PG::Constants::PQTRANS_INERROR => 'INERROR',
391
- PG::Constants::PQTRANS_UNKNOWN => 'UNKNOWN' }
392
261
  conn.instance_variable_set(:@pgtk_pid, conn.backend_pid)
393
262
  parts = [
394
263
  ' ',
395
264
  "##{conn.backend_pid}",
396
- pipelines.fetch(conn.pipeline_status, "pipeline_status=#{conn.pipeline_status}"),
397
- statuses.fetch(conn.status, "status=#{conn.status}"),
398
- transactions.fetch(conn.transaction_status, "transaction_status=#{conn.transaction_status}")
265
+ {
266
+ PG::Constants::PQ_PIPELINE_ON => 'ON', PG::Constants::PQ_PIPELINE_OFF => 'OFF',
267
+ PG::Constants::PQ_PIPELINE_ABORTED => 'ABORTED'
268
+ }.fetch(conn.pipeline_status, "pipeline_status=#{conn.pipeline_status}"),
269
+ { PG::Constants::CONNECTION_OK => 'OK', PG::Constants::CONNECTION_BAD => 'BAD' }.fetch(
270
+ conn.status,
271
+ "status=#{conn.status}"
272
+ ),
273
+ {
274
+ PG::Constants::PQTRANS_IDLE => 'IDLE', PG::Constants::PQTRANS_ACTIVE => 'ACTIVE',
275
+ PG::Constants::PQTRANS_INTRANS => 'INTRANS', PG::Constants::PQTRANS_INERROR => 'INERROR',
276
+ PG::Constants::PQTRANS_UNKNOWN => 'UNKNOWN'
277
+ }.fetch(conn.transaction_status, "transaction_status=#{conn.transaction_status}")
399
278
  ]
400
279
  if conn.transaction_status != PG::Constants::PQTRANS_IDLE
401
280
  started = conn.instance_variable_get(:@pgtk_started_at)
@@ -434,3 +313,7 @@ class Pgtk::Pool
434
313
  @wire.connection
435
314
  end
436
315
  end
316
+
317
+ require_relative 'pool/busy'
318
+ require_relative 'pool/iterable_queue'
319
+ require_relative 'pool/txn'
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require_relative '../../pgtk'
7
+
8
+ class Pgtk::Retry; end
9
+
10
+ # Raised when all retry attempts have been exhausted. The original
11
+ # exception that caused the last failure is available via #cause,
12
+ # so its message and stack trace are preserved for debugging.
13
+ #
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
16
+ # License:: MIT
17
+ class Pgtk::Retry::Exhausted < StandardError; end
data/lib/pgtk/retry.rb CHANGED
@@ -49,11 +49,6 @@ require_relative 'impatient'
49
49
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
50
50
  # License:: MIT
51
51
  class Pgtk::Retry
52
- # Raised when all retry attempts have been exhausted. The original
53
- # exception that caused the last failure is available via #cause,
54
- # so its message and stack trace are preserved for debugging.
55
- class Exhausted < StandardError; end
56
-
57
52
  BACKOFFS = [0.05, 0.2, 1.0].freeze
58
53
 
59
54
  # Constructor.
@@ -122,3 +117,5 @@ class Pgtk::Retry
122
117
  @pool.transaction(&)
123
118
  end
124
119
  end
120
+
121
+ require_relative 'retry/exhausted'
data/lib/pgtk/spy.rb CHANGED
@@ -82,10 +82,8 @@ class Pgtk::Spy
82
82
  # @param [String] sql The SQL query with params inside (possibly)
83
83
  # @return [Array] Result rows
84
84
  def exec(sql, *)
85
- start = Time.now
86
- ret = @pool.exec(sql, *)
87
- @block&.call(sql.is_a?(Array) ? sql.join(' ') : sql, Time.now - start)
88
- ret
85
+ @block&.call(sql.is_a?(Array) ? sql.join(' ') : sql, Time.now - Time.now)
86
+ @pool.exec(sql, *)
89
87
  end
90
88
 
91
89
  # Run a transaction with spying on each SQL query.
data/lib/pgtk/stash.rb CHANGED
@@ -109,8 +109,7 @@ class Pgtk::Stash
109
109
  # @return [String] Multi-line text representation of the current cache state
110
110
  def dump
111
111
  @entrance.with_read_lock do
112
- qq = queries
113
- body(qq)
112
+ body(queries)
114
113
  end
115
114
  end
116
115
 
@@ -227,22 +226,20 @@ class Pgtk::Stash
227
226
  affected.each { |t| @stash[:table_inflight][t] = (@stash[:table_inflight][t] || 0) + 1 }
228
227
  end
229
228
  begin
230
- ret = @pool.exec(pure, params, result)
231
- now = Time.now
232
- @entrance.with_write_lock do
233
- affected.each do |t|
234
- @stash[:table_inflight][t] -= 1
235
- old = @stash[:table_mod][t]
236
- stamp = old && old > now ? old : now
237
- @stash[:table_mod][t] = stamp
238
- @stash[:tables][t]&.each do |q|
239
- @stash[:queries][q]&.each_key do |key|
240
- @stash[:queries][q][key][:stale] = stamp
229
+ @pool.exec(pure, params, result).tap do
230
+ now = Time.now
231
+ @entrance.with_write_lock do
232
+ affected.each do |t|
233
+ @stash[:table_inflight][t] -= 1
234
+ @stash[:table_mod][t] = (@stash[:table_mod][t] || 0) + 1
235
+ @stash[:tables][t]&.each do |q|
236
+ @stash[:queries][q]&.each_key do |key|
237
+ @stash[:queries][q][key][:stale] = now
238
+ end
241
239
  end
242
240
  end
243
241
  end
244
242
  end
245
- ret
246
243
  rescue StandardError
247
244
  @entrance.with_write_lock do
248
245
  affected.each { |t| @stash[:table_inflight][t] -= 1 }
@@ -306,8 +303,6 @@ class Pgtk::Stash
306
303
 
307
304
  # Discover ON DELETE CASCADE / ON UPDATE CASCADE foreign keys so that a
308
305
  # modify on the parent table also invalidates cached queries on children.
309
- #
310
- # @return [nil]
311
306
  def cascade!
312
307
  direct = Hash.new { |h, k| h[k] = [] }
313
308
  @pool.exec(<<~SQL).each { |r| direct[r['parent']] << r['child'] }
@@ -324,7 +319,6 @@ class Pgtk::Stash
324
319
  AND tc.table_schema NOT IN ('pg_catalog', 'information_schema')
325
320
  SQL
326
321
  @cascades = direct.keys.to_h { |p| [p, transitive(p, direct, []).uniq] }
327
- nil
328
322
  end
329
323
 
330
324
  def transitive(parent, direct, seen)
data/lib/pgtk/version.rb CHANGED
@@ -10,5 +10,5 @@ require_relative '../pgtk'
10
10
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
11
11
  # License:: MIT
12
12
  module Pgtk
13
- VERSION = '0.32.4' unless defined?(VERSION)
13
+ VERSION = '0.32.5' unless defined?(VERSION)
14
14
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'pg'
7
+ require_relative '../../pgtk'
8
+
9
+ module Pgtk::Wire; end
10
+
11
+ # Simple wire with details.
12
+ #
13
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
14
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
15
+ # License:: MIT
16
+ class Pgtk::Wire::Direct
17
+ # Constructor.
18
+ #
19
+ # @param [String] host Host name of the PostgreSQL server
20
+ # @param [Integer] port Port number of the PostgreSQL server
21
+ # @param [String] dbname Database name
22
+ # @param [String] user Username
23
+ # @param [String] password Password
24
+ # @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
25
+ # +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+)
26
+ def initialize(host:, port:, dbname:, user:, password:, **opts)
27
+ raise(ArgumentError, "The host can't be nil") if host.nil?
28
+ @host = host
29
+ raise(ArgumentError, "The port can't be nil") if port.nil?
30
+ @port = port
31
+ @dbname = dbname
32
+ @user = user
33
+ @password = password
34
+ @opts = opts
35
+ end
36
+
37
+ # Create a new connection to PostgreSQL server.
38
+ def connection
39
+ PG.connect(dbname: @dbname, host: @host, port: @port, user: @user, password: @password, **@opts)
40
+ end
41
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'cgi'
7
+ require 'uri'
8
+ require_relative '../../pgtk'
9
+ require_relative 'direct'
10
+
11
+ module Pgtk::Wire; end
12
+
13
+ # Using ENV variable.
14
+ #
15
+ # The value of the variable should be in this format:
16
+ #
17
+ # postgres://user:password@host:port/dbname
18
+ #
19
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
20
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
21
+ # License:: MIT
22
+ class Pgtk::Wire::Env
23
+ # Constructor.
24
+ #
25
+ # @param [String] var The name of the environment variable with the connection URL
26
+ # @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
27
+ # +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+).
28
+ # Explicit kwargs win over options carried in the URL query string on conflict.
29
+ def initialize(var = 'DATABASE_URL', **opts)
30
+ raise(ArgumentError, "The name of the environment variable can't be nil") if var.nil?
31
+ @value = ENV.fetch(var, nil)
32
+ raise(ArgumentError, "The environment variable #{@value.inspect} is not set") if @value.nil?
33
+ @opts = opts
34
+ end
35
+
36
+ # Create a new connection to PostgreSQL server.
37
+ def connection
38
+ uri = URI(@value)
39
+ Pgtk::Wire::Direct.new(
40
+ host: CGI.unescape(uri.host),
41
+ port: uri.port || 5432,
42
+ dbname: CGI.unescape(uri.path[1..]),
43
+ user: CGI.unescape(uri.userinfo.split(':')[0]),
44
+ password: CGI.unescape(uri.userinfo.split(':')[1]),
45
+ **(uri.query ? URI.decode_www_form(uri.query).to_h.transform_keys(&:to_sym) : {}).merge(@opts)
46
+ ).connection
47
+ end
48
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
+ # SPDX-License-Identifier: MIT
5
+
6
+ require 'yaml'
7
+ require_relative '../../pgtk'
8
+ require_relative 'direct'
9
+
10
+ module Pgtk::Wire; end
11
+
12
+ # Using configuration from YAML file.
13
+ #
14
+ # Author:: Yegor Bugayenko (yegor256@gmail.com)
15
+ # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
16
+ # License:: MIT
17
+ class Pgtk::Wire::Yaml
18
+ # Constructor.
19
+ #
20
+ # @param [String] file Path to the YAML configuration file
21
+ # @param [String] node The root node name in the YAML file containing PostgreSQL configuration
22
+ def initialize(file, node = 'pgsql')
23
+ raise(ArgumentError, "The name of the file can't be nil") if file.nil?
24
+ @file = file
25
+ raise(ArgumentError, "The name of the node in the YAML file can't be nil") if node.nil?
26
+ @node = node
27
+ end
28
+
29
+ # Create a new connection to PostgreSQL server.
30
+ def connection
31
+ raise(ArgumentError, "The file #{@file.inspect} not found") unless File.exist?(@file)
32
+ cfg = ::YAML.load_file(@file)
33
+ raise(ArgumentError, "The node '#{@node}' not found in YAML file #{@file.inspect}") unless cfg[@node]
34
+ Pgtk::Wire::Direct.new(
35
+ host: cfg[@node]['host'],
36
+ port: cfg[@node]['port'],
37
+ dbname: cfg[@node]['dbname'],
38
+ user: cfg[@node]['user'],
39
+ password: cfg[@node]['password']
40
+ ).connection
41
+ end
42
+ end
data/lib/pgtk/wire.rb CHANGED
@@ -3,10 +3,6 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'cgi'
7
- require 'pg'
8
- require 'uri'
9
- require 'yaml'
10
6
  require_relative '../pgtk'
11
7
 
12
8
  # Wires.
@@ -14,103 +10,8 @@ require_relative '../pgtk'
14
10
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
15
11
  # License:: MIT
16
12
  module Pgtk::Wire
17
- # Simple wire with details.
18
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
19
- # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
20
- # License:: MIT
21
- class Direct
22
- # Constructor.
23
- #
24
- # @param [String] host Host name of the PostgreSQL server
25
- # @param [Integer] port Port number of the PostgreSQL server
26
- # @param [String] dbname Database name
27
- # @param [String] user Username
28
- # @param [String] password Password
29
- # @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
30
- # +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+)
31
- def initialize(host:, port:, dbname:, user:, password:, **opts)
32
- raise(ArgumentError, "The host can't be nil") if host.nil?
33
- @host = host
34
- raise(ArgumentError, "The port can't be nil") if port.nil?
35
- @port = port
36
- @dbname = dbname
37
- @user = user
38
- @password = password
39
- @opts = opts
40
- end
41
-
42
- # Create a new connection to PostgreSQL server.
43
- def connection
44
- PG.connect(dbname: @dbname, host: @host, port: @port, user: @user, password: @password, **@opts)
45
- end
46
- end
47
-
48
- # Using ENV variable.
49
- #
50
- # The value of the variable should be in this format:
51
- #
52
- # postgres://user:password@host:port/dbname
53
- #
54
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
55
- # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
56
- # License:: MIT
57
- class Env
58
- # Constructor.
59
- #
60
- # @param [String] var The name of the environment variable with the connection URL
61
- # @param [Hash] opts Extra options forwarded to +PG.connect+ (e.g. +sslmode+,
62
- # +connect_timeout+, +keepalives+, +keepalives_idle+, +application_name+).
63
- # Explicit kwargs win over options carried in the URL query string on conflict.
64
- def initialize(var = 'DATABASE_URL', **opts)
65
- raise(ArgumentError, "The name of the environment variable can't be nil") if var.nil?
66
- @value = ENV.fetch(var, nil)
67
- raise(ArgumentError, "The environment variable #{@value.inspect} is not set") if @value.nil?
68
- @opts = opts
69
- end
70
-
71
- # Create a new connection to PostgreSQL server.
72
- def connection
73
- uri = URI(@value)
74
- extras = uri.query ? URI.decode_www_form(uri.query).to_h.transform_keys(&:to_sym) : {}
75
- Pgtk::Wire::Direct.new(
76
- host: CGI.unescape(uri.host),
77
- port: uri.port || 5432,
78
- dbname: CGI.unescape(uri.path[1..]),
79
- user: CGI.unescape(uri.userinfo.split(':')[0]),
80
- password: CGI.unescape(uri.userinfo.split(':')[1]),
81
- **extras.merge(@opts)
82
- ).connection
83
- end
84
- end
85
-
86
- # Using configuration from YAML file.
87
- # Author:: Yegor Bugayenko (yegor256@gmail.com)
88
- # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
89
- # License:: MIT
90
- class Yaml
91
- # Constructor.
92
- #
93
- # @param [String] file Path to the YAML configuration file
94
- # @param [String] node The root node name in the YAML file containing PostgreSQL configuration
95
- def initialize(file, node = 'pgsql')
96
- raise(ArgumentError, "The name of the file can't be nil") if file.nil?
97
- @file = file
98
- raise(ArgumentError, "The name of the node in the YAML file can't be nil") if node.nil?
99
- @node = node
100
- end
101
-
102
- # Create a new connection to PostgreSQL server.
103
- def connection
104
- raise(ArgumentError, "The file #{@file.inspect} not found") unless File.exist?(@file)
105
- cfg = YAML.load_file(@file)
106
- raise(ArgumentError, "The node '#{@node}' not found in YAML file #{@file.inspect}") unless cfg[@node]
107
- Pgtk::Wire::Direct.new(
108
- host: cfg[@node]['host'],
109
- port: cfg[@node]['port'],
110
- dbname: cfg[@node]['dbname'],
111
- user: cfg[@node]['user'],
112
- password: cfg[@node]['password']
113
- ).connection
114
- end
115
- end
116
13
  end
14
+
15
+ require_relative 'wire/direct'
16
+ require_relative 'wire/env'
17
+ require_relative 'wire/yaml'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgtk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.32.4
4
+ version: 0.32.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -197,15 +197,24 @@ files:
197
197
  - cucumber.yml
198
198
  - lib/pgtk.rb
199
199
  - lib/pgtk/impatient.rb
200
+ - lib/pgtk/impatient/too_slow.rb
200
201
  - lib/pgtk/liquibase_task.rb
201
202
  - lib/pgtk/liquicheck_task.rb
203
+ - lib/pgtk/liquicheck_task/must_error.rb
202
204
  - lib/pgtk/pgsql_task.rb
203
205
  - lib/pgtk/pool.rb
206
+ - lib/pgtk/pool/busy.rb
207
+ - lib/pgtk/pool/iterable_queue.rb
208
+ - lib/pgtk/pool/txn.rb
204
209
  - lib/pgtk/retry.rb
210
+ - lib/pgtk/retry/exhausted.rb
205
211
  - lib/pgtk/spy.rb
206
212
  - lib/pgtk/stash.rb
207
213
  - lib/pgtk/version.rb
208
214
  - lib/pgtk/wire.rb
215
+ - lib/pgtk/wire/direct.rb
216
+ - lib/pgtk/wire/env.rb
217
+ - lib/pgtk/wire/yaml.rb
209
218
  - pgtk.gemspec
210
219
  - resources/pom.xml
211
220
  - test-resources/2019/01-test.xml