pgtk 0.30.5 → 0.30.7

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.
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'English'
3
4
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
5
  # SPDX-License-Identifier: MIT
5
6
 
6
7
  require 'cgi'
7
- require 'English'
8
8
  require 'qbash'
9
9
  require 'rake'
10
10
  require 'rake/tasklib'
11
11
  require 'random-port'
12
- require 'shellwords'
13
12
  require 'securerandom'
13
+ require 'shellwords'
14
14
  require 'tempfile'
15
- require 'yaml'
16
15
  require 'waitutil'
16
+ require 'yaml'
17
17
  require_relative '../pgtk'
18
18
 
19
19
  # Pgsql rake task.
@@ -21,49 +21,7 @@ require_relative '../pgtk'
21
21
  # Copyright:: Copyright (c) 2019-2026 Yegor Bugayenko
22
22
  # License:: MIT
23
23
  class Pgtk::PgsqlTask < Rake::TaskLib
24
- # Task name
25
- # @return [Symbol]
26
- attr_accessor :name
27
-
28
- # Directory where PostgreSQL server files will be stored
29
- # @return [String]
30
- attr_accessor :dir
31
-
32
- # Whether to delete the PostgreSQL data directory on each run
33
- # @return [Boolean]
34
- attr_accessor :fresh_start
35
-
36
- # PostgreSQL username
37
- # @return [String]
38
- attr_accessor :user
39
-
40
- # PostgreSQL password
41
- # @return [String]
42
- attr_accessor :password
43
-
44
- # PostgreSQL database name
45
- # @return [String]
46
- attr_accessor :dbname
47
-
48
- # Path to YAML file where configuration will be written
49
- # @return [String]
50
- attr_accessor :yaml
51
-
52
- # Whether to suppress output
53
- # @return [Boolean]
54
- attr_accessor :quiet
55
-
56
- # TCP port for PostgreSQL server (random if nil)
57
- # @return [Integer, nil]
58
- attr_accessor :port
59
-
60
- # Configuration options for PostgreSQL server
61
- # @return [Hash]
62
- attr_accessor :config
63
-
64
- # Use docker (set to either :never, :always, or :maybe)
65
- # @return [Symbol]
66
- attr_accessor :docker
24
+ attr_accessor :name, :dir, :fresh, :user, :password, :dbname, :yaml, :quiet, :port, :config, :docker
67
25
 
68
26
  # Initialize a new PostgreSQL server task.
69
27
  #
@@ -73,16 +31,16 @@ class Pgtk::PgsqlTask < Rake::TaskLib
73
31
  super()
74
32
  @docker ||= :maybe
75
33
  @name = args.shift || :pgsql
76
- @fresh_start = false
34
+ @fresh = false
77
35
  @quiet = false
78
36
  @user = 'test'
79
37
  @config = {}
80
38
  @password = 'test'
81
39
  @dbname = 'test'
82
40
  @port = nil
83
- desc 'Start a local PostgreSQL server' unless ::Rake.application.last_description
41
+ desc('Start a local PostgreSQL server') unless ::Rake.application.last_description
84
42
  task(name, *args) do |_, task_args|
85
- RakeFileUtils.send(:verbose, true) do
43
+ RakeFileUtils.verbose(true) do
86
44
  yield(*[self, task_args].slice(0, task_block.arity)) if block_given?
87
45
  run
88
46
  end
@@ -92,35 +50,52 @@ class Pgtk::PgsqlTask < Rake::TaskLib
92
50
  private
93
51
 
94
52
  def run
95
- pg_out = qbash('postgres -V; initdb -V', accept: nil, both: true)
96
- local = pg_out[1].zero?
97
- docker_out = qbash('docker -v', accept: nil, both: true)
98
- docker = docker_out[1].zero?
99
- unless local || docker
100
- raise \
101
- "Failed to find either PostgreSQL or Docker:\n#{pg_out.first}\n#{docker_out.first}"
102
- end
103
- raise 'You cannot force Docker to run, because it is not installed locally' if @docker == :always && !docker
104
- raise "Option 'dir' is mandatory" unless @dir
105
- raise "Option 'yaml' is mandatory" unless @yaml
53
+ local = detect(:local)
54
+ docker = detect(:docker)
55
+ preflight(local, docker)
106
56
  home = File.expand_path(@dir)
107
- FileUtils.rm_rf(home) if @fresh_start
108
- raise "Directory/file #{home} is present, use fresh_start=true" if File.exist?(home)
57
+ FileUtils.rm_rf(home) if @fresh
58
+ raise(ArgumentError, "Directory/file #{home} is present, use fresh=true") if File.exist?(home)
109
59
  stdout = @quiet ? nil : $stdout
60
+ 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}")
66
+ end
67
+
68
+ def preflight(local, docker)
69
+ raise(IOError, 'Failed to find either PostgreSQL or Docker') unless local || docker
70
+ if @docker == :always && !docker
71
+ raise(ArgumentError, 'You cannot force Docker to run, because it is not installed locally')
72
+ end
73
+ raise(ArgumentError, "Option 'dir' is mandatory") unless @dir
74
+ raise(ArgumentError, "Option 'yaml' is mandatory") unless @yaml
75
+ end
76
+
77
+ def acquire
110
78
  port = @port
111
79
  if port.nil?
112
80
  port = RandomPort::Pool::SINGLETON.acquire
113
- puts "Random TCP port #{port} is used for PostgreSQL server" unless @quiet
81
+ puts("Random TCP port #{port} is used for PostgreSQL server") unless @quiet
114
82
  else
115
- puts "Required TCP port #{port} is used for PostgreSQL server" unless @quiet
83
+ puts("Required TCP port #{port} is used for PostgreSQL server") unless @quiet
116
84
  end
85
+ port
86
+ end
87
+
88
+ def launch(local, home, stdout, port)
117
89
  if (local && @docker != :always) || @docker == :never
118
- pid = run_local(home, stdout, port)
119
- place = "in process ##{pid}"
90
+ pid = localize(home, stdout, port)
91
+ "in process ##{pid}"
120
92
  else
121
- container = run_docker(home, stdout, port)
122
- place = "in container #{container}"
93
+ container = dockerize(home, stdout, port)
94
+ "in container #{container}"
123
95
  end
96
+ end
97
+
98
+ def save(port)
124
99
  File.write(
125
100
  @yaml,
126
101
  {
@@ -130,19 +105,22 @@ class Pgtk::PgsqlTask < Rake::TaskLib
130
105
  'dbname' => @dbname,
131
106
  'user' => @user,
132
107
  'password' => @password,
133
- 'url' => [
134
- "jdbc:postgresql://localhost:#{port}/",
135
- "#{CGI.escape(@dbname)}?user=#{CGI.escape(@user)}"
136
- ].join
108
+ 'url' => ["jdbc:postgresql://localhost:#{port}/", "#{CGI.escape(@dbname)}?user=#{CGI.escape(@user)}"].join
137
109
  }
138
110
  }.to_yaml
139
111
  )
140
- return if @quiet
141
- puts "PostgreSQL has been started #{place}, port #{port}"
142
- puts "YAML config saved to #{@yaml}"
143
112
  end
144
113
 
145
- def run_docker(home, stdout, port)
114
+ def detect(what)
115
+ case what
116
+ when :local
117
+ qbash('postgres -V; initdb -V', accept: nil, both: true)[1].zero?
118
+ when :docker
119
+ qbash('docker -v', accept: nil, both: true)[1].zero?
120
+ end
121
+ end
122
+
123
+ def dockerize(home, stdout, port)
146
124
  FileUtils.mkdir_p(home)
147
125
  out =
148
126
  qbash(
@@ -166,18 +144,18 @@ class Pgtk::PgsqlTask < Rake::TaskLib
166
144
  both: true, accept: nil
167
145
  )[1].zero?
168
146
  qbash("docker stop #{Shellwords.escape(container)}")
169
- puts "PostgreSQL docker container #{container.inspect} was stopped" unless @quiet
147
+ puts("PostgreSQL docker container #{container.inspect} was stopped") unless @quiet
170
148
  end
171
149
  end
172
150
  begin
173
151
  WaitUtil.wait_for_service('PG in Docker', 'localhost', port, timeout_sec: 10, delay_sec: 0.1)
174
152
  rescue WaitUtil::TimeoutError => e
175
- raise "Failed to start PostgreSQL Docker container #{container.inspect}: #{e.message}"
153
+ raise(IOError, "Failed to start PostgreSQL Docker container #{container.inspect}: #{e.message}")
176
154
  end
177
155
  container
178
156
  end
179
157
 
180
- def run_local(home, stdout, port)
158
+ def localize(home, stdout, port)
181
159
  Tempfile.open do |pwfile|
182
160
  File.write(pwfile.path, @password)
183
161
  qbash(
@@ -200,23 +178,19 @@ class Pgtk::PgsqlTask < Rake::TaskLib
200
178
  @config.map { |k, v| "-c #{Shellwords.escape("#{k}=#{v}")}" },
201
179
  "--port=#{port}"
202
180
  ].join(' ')
203
- pid = Process.spawn(
204
- cmd,
205
- $stdout => File.join(home, 'stdout.txt'),
206
- $stderr => File.join(home, 'stderr.txt')
207
- )
181
+ pid = Process.spawn(cmd, $stdout => File.join(home, 'stdout.txt'), $stderr => File.join(home, 'stderr.txt'))
208
182
  File.write(File.join(@dir, 'pid'), pid)
209
183
  at_exit do
210
184
  qbash("kill -TERM #{Shellwords.escape(pid)}", stdout:)
211
- puts "PostgreSQL killed in PID #{pid}" unless @quiet
185
+ puts("PostgreSQL killed in PID #{pid}") unless @quiet
212
186
  end
213
187
  begin
214
188
  WaitUtil.wait_for_service('PG in local', 'localhost', port, timeout_sec: 10, delay_sec: 0.1)
215
189
  rescue WaitUtil::TimeoutError => e
216
- puts "+ #{cmd}"
217
- puts "stdout:\n#{File.read(File.join(home, 'stdout.txt'))}"
218
- puts "stderr:\n#{File.read(File.join(home, 'stderr.txt'))}"
219
- raise "Failed to start PostgreSQL database server on port #{port}: #{e.message}"
190
+ puts("+ #{cmd}")
191
+ puts("stdout:\n#{File.read(File.join(home, 'stdout.txt'))}")
192
+ puts("stderr:\n#{File.read(File.join(home, 'stderr.txt'))}")
193
+ raise(IOError, "Failed to start PostgreSQL database server on port #{port}: #{e.message}")
220
194
  end
221
195
  qbash(
222
196
  'createdb',
data/lib/pgtk/pool.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'loog'
3
4
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
5
  # SPDX-License-Identifier: MIT
5
6
 
6
7
  require 'pg'
7
- require 'loog'
8
8
  require 'tago'
9
9
  require_relative '../pgtk'
10
10
  require_relative 'version'
@@ -130,7 +130,7 @@ class Pgtk::Pool
130
130
  def start!
131
131
  return if @started
132
132
  @max.times do
133
- @pool << @wire.connection
133
+ @pool.push(@wire.connection)
134
134
  end
135
135
  @started = true
136
136
  @log.debug("PostgreSQL pool started with #{@max} connections")
@@ -210,12 +210,12 @@ class Pgtk::Pool
210
210
  t = Txn.new(c, @log)
211
211
  t.exec('START TRANSACTION')
212
212
  begin
213
- r = yield t
213
+ r = yield(t)
214
214
  t.exec('COMMIT')
215
215
  r
216
216
  rescue StandardError => e
217
217
  t.exec('ROLLBACK')
218
- raise e
218
+ raise(e)
219
219
  end
220
220
  end
221
221
  end
@@ -239,7 +239,7 @@ class Pgtk::Pool
239
239
  @condition = ConditionVariable.new
240
240
  end
241
241
 
242
- def <<(item)
242
+ def push(item)
243
243
  @mutex.synchronize do
244
244
  if @items.size < @size
245
245
  @items << item
@@ -248,7 +248,7 @@ class Pgtk::Pool
248
248
  index = @items.index(item)
249
249
  if index.nil?
250
250
  index = @taken.index(true)
251
- raise 'No taken slot found' if index.nil?
251
+ raise(StandardError, 'No taken slot found') if index.nil?
252
252
  @items[index] = item
253
253
  end
254
254
  @taken[index] = false
@@ -299,7 +299,7 @@ class Pgtk::Pool
299
299
  if args.empty?
300
300
  @conn.exec(sql) do |res|
301
301
  if block_given?
302
- yield res
302
+ yield(res)
303
303
  else
304
304
  res.each.to_a
305
305
  end
@@ -307,7 +307,7 @@ class Pgtk::Pool
307
307
  else
308
308
  @conn.exec_params(sql, args, result) do |res|
309
309
  if block_given?
310
- yield res
310
+ yield(res)
311
311
  else
312
312
  res.each.to_a
313
313
  end
@@ -315,7 +315,7 @@ class Pgtk::Pool
315
315
  end
316
316
  rescue StandardError => e
317
317
  @log.error("#{sql} -> #{e.message}")
318
- raise e
318
+ raise(e)
319
319
  end
320
320
  lag = Time.now - start
321
321
  if lag < 1
@@ -332,12 +332,12 @@ class Pgtk::Pool
332
332
  def connect
333
333
  conn = @pool.pop
334
334
  begin
335
- yield conn
335
+ yield(conn)
336
336
  rescue StandardError => e
337
337
  conn = renew(conn)
338
- raise e
338
+ raise(e)
339
339
  ensure
340
- @pool << conn
340
+ @pool.push(conn)
341
341
  end
342
342
  end
343
343
 
data/lib/pgtk/retry.rb CHANGED
@@ -79,22 +79,25 @@ class Pgtk::Retry
79
79
  end
80
80
 
81
81
  # Execute a SQL query with automatic retry for SELECT queries.
82
+ # Also retries PG::ConnectionBad errors for all query types, since
83
+ # connection errors indicate the query never reached PostgreSQL.
82
84
  #
83
85
  # @param [String] sql The SQL query with params inside (possibly)
84
86
  # @return [Array] Result rows
85
87
  def exec(sql, *)
86
88
  query = sql.is_a?(Array) ? sql.join(' ') : sql
87
- if query.strip.upcase.start_with?('SELECT')
88
- attempt = 0
89
- begin
90
- @pool.exec(sql, *)
91
- rescue StandardError => e
92
- attempt += 1
93
- raise e if attempt >= @attempts
94
- retry
95
- end
96
- else
89
+ attempt = 0
90
+ begin
97
91
  @pool.exec(sql, *)
92
+ rescue PG::ConnectionBad => e
93
+ attempt += 1
94
+ raise(e) if attempt >= @attempts
95
+ retry
96
+ rescue StandardError => e
97
+ raise(e) unless query.strip.upcase.start_with?('SELECT')
98
+ attempt += 1
99
+ raise(e) if attempt >= @attempts
100
+ retry
98
101
  end
99
102
  end
100
103
 
data/lib/pgtk/spy.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'loog'
3
4
  # SPDX-FileCopyrightText: Copyright (c) 2019-2026 Yegor Bugayenko
4
5
  # SPDX-License-Identifier: MIT
5
6
 
6
7
  require 'pg'
7
- require 'loog'
8
8
  require_relative '../pgtk'
9
9
  require_relative 'wire'
10
10
 
@@ -94,7 +94,7 @@ class Pgtk::Spy
94
94
  # @return [Object] Result of the block
95
95
  def transaction
96
96
  @pool.transaction do |t|
97
- yield Pgtk::Spy.new(t, &@block)
97
+ yield(Pgtk::Spy.new(t, &@block))
98
98
  end
99
99
  end
100
100
  end