pgtk 0.32.4 → 0.32.6

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: bff4002fc7f88a973b99757aa0b8be3b82b6631b87f94c8ade93b30983b21761
4
+ data.tar.gz: 707d5ec1bf05047d60a0f25103b7210751c4731f79caa8b08c3021e7e3810e6c
5
5
  SHA512:
6
- metadata.gz: 691ea0c8b5ca6002b72ab4b66487b3dbf489feaedbc8e80cc772fe69cfc1ad966fa836e13298b561cadb38c75290ed8a533a389751a7d6b0454da23b87afcb74
7
- data.tar.gz: 8a04b05ad839b935ac3f8e5362bb957eb64e8e70e1b963c5713a7c7755f8184e6c1e8c806a4d03467bba80e3ce5af0cf9c1a1a50c4d73f11e79d46dc9569fc2f
6
+ metadata.gz: 210230feda441fdee55cb3c3b6ccf1e2335efcdef77e95996afe89d3a4caf4f21bd8aa4fb9fd19f9b636d1336cc821345aa7454ec474d1693dff21990f2a1b40
7
+ data.tar.gz: ab98c8f9cf29405c495bd4808855e1107889ccdc2ee9011f4abe431b561f05d033564dc587f5a3084f64f0842e935d4bee6b9c8265cc729fe762a0f99bab4eb9
data/Gemfile.lock CHANGED
@@ -2,7 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  pgtk (0.0.0)
5
- backtrace (~> 0.4)
5
+ cgi (~> 0.0)
6
6
  concurrent-ruby (~> 1.3)
7
7
  donce (~> 0.0)
8
8
  ellipsized (~> 0.3)
@@ -22,7 +22,8 @@ GEM
22
22
  ast (2.4.3)
23
23
  backtrace (0.4.1)
24
24
  builder (3.3.0)
25
- concurrent-ruby (1.3.6)
25
+ cgi (0.5.1)
26
+ concurrent-ruby (1.3.7)
26
27
  differ (0.1.2)
27
28
  docile (1.4.1)
28
29
  donce (0.7.0)
@@ -35,7 +36,7 @@ GEM
35
36
  tago (~> 0.1)
36
37
  ellipsized (0.3.0)
37
38
  joined (0.4.0)
38
- json (2.19.5)
39
+ json (2.19.9)
39
40
  language_server-protocol (3.17.0.5)
40
41
  lint_roller (1.1.0)
41
42
  logger (1.7.0)
@@ -52,24 +53,24 @@ GEM
52
53
  minitest (>= 5.0, < 7)
53
54
  ruby-progressbar
54
55
  minitest-stub-const (0.6)
55
- nokogiri (1.19.3-aarch64-linux-gnu)
56
+ nokogiri (1.19.4-aarch64-linux-gnu)
56
57
  racc (~> 1.4)
57
- nokogiri (1.19.3-aarch64-linux-musl)
58
+ nokogiri (1.19.4-aarch64-linux-musl)
58
59
  racc (~> 1.4)
59
- nokogiri (1.19.3-arm-linux-gnu)
60
+ nokogiri (1.19.4-arm-linux-gnu)
60
61
  racc (~> 1.4)
61
- nokogiri (1.19.3-arm-linux-musl)
62
+ nokogiri (1.19.4-arm-linux-musl)
62
63
  racc (~> 1.4)
63
- nokogiri (1.19.3-arm64-darwin)
64
+ nokogiri (1.19.4-arm64-darwin)
64
65
  racc (~> 1.4)
65
- nokogiri (1.19.3-x86_64-darwin)
66
+ nokogiri (1.19.4-x86_64-darwin)
66
67
  racc (~> 1.4)
67
- nokogiri (1.19.3-x86_64-linux-gnu)
68
+ nokogiri (1.19.4-x86_64-linux-gnu)
68
69
  racc (~> 1.4)
69
- nokogiri (1.19.3-x86_64-linux-musl)
70
+ nokogiri (1.19.4-x86_64-linux-musl)
70
71
  racc (~> 1.4)
71
72
  os (1.1.4)
72
- parallel (2.0.1)
73
+ parallel (2.1.0)
73
74
  parser (3.3.11.1)
74
75
  ast (~> 2.4.1)
75
76
  racc
@@ -81,7 +82,7 @@ GEM
81
82
  pg (1.6.3-x86_64-linux)
82
83
  pg (1.6.3-x86_64-linux-musl)
83
84
  prism (1.9.0)
84
- qbash (0.8.3)
85
+ qbash (0.8.4)
85
86
  backtrace (> 0)
86
87
  elapsed (> 0)
87
88
  loog (> 0)
@@ -94,7 +95,7 @@ GEM
94
95
  tago (~> 0.0)
95
96
  regexp_parser (2.12.0)
96
97
  rexml (3.4.4)
97
- rubocop (1.86.2)
98
+ rubocop (1.87.0)
98
99
  json (~> 2.3)
99
100
  language_server-protocol (~> 3.17.0.2)
100
101
  lint_roller (~> 1.1.0)
@@ -108,7 +109,7 @@ GEM
108
109
  rubocop-ast (1.49.1)
109
110
  parser (>= 3.3.7.2)
110
111
  prism (~> 1.7)
111
- rubocop-elegant (0.0.20)
112
+ rubocop-elegant (0.6.0)
112
113
  lint_roller (~> 1.1)
113
114
  rubocop (~> 1.75)
114
115
  rubocop-minitest (0.39.1)
@@ -147,7 +148,7 @@ GEM
147
148
  nokogiri (~> 1.8)
148
149
  rainbow (~> 3.0)
149
150
  slop (~> 4.4)
150
- yard (0.9.43)
151
+ yard (0.9.44)
151
152
 
152
153
  PLATFORMS
153
154
  aarch64-linux
@@ -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,18 +58,17 @@ 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
67
64
  # @param [Integer] timeout Timeout in seconds for each SQL query
68
65
  # @param [Array<Regex>] off List of regex to exclude queries from checking
69
- def initialize(pool, timeout, *off)
66
+ # @param [Integer] default Fallback timeout in seconds for excluded queries (0 = no timeout)
67
+ def initialize(pool, timeout, *off, default: 300)
70
68
  @pool = pool
71
69
  @timeout = timeout
72
70
  @off = off
71
+ @default = default
73
72
  end
74
73
 
75
74
  # Start a new connection pool with the given arguments.
@@ -89,7 +88,7 @@ class Pgtk::Impatient
89
88
  [
90
89
  @pool.dump,
91
90
  '',
92
- "Pgtk::Impatient (timeout=#{@timeout}s):",
91
+ "Pgtk::Impatient (timeout=#{@timeout}s, default=#{@default}s):",
93
92
  @off.map { |re| " #{re}" }
94
93
  ].join("\n")
95
94
  end
@@ -109,7 +108,13 @@ class Pgtk::Impatient
109
108
  # @raise [TooSlow] If the query takes too long
110
109
  def exec(query, *args)
111
110
  sql = query.is_a?(Array) ? query.join(' ') : query
112
- return @pool.exec(sql, *args) if @off.any? { |re| re.match?(sql) }
111
+ if @off.any? { |re| re.match?(sql) }
112
+ ms = Integer(@default * 1000)
113
+ return @pool.transaction do |t|
114
+ t.exec("SET LOCAL statement_timeout = #{ms}") unless ms.zero?
115
+ t.exec(sql, *args)
116
+ end
117
+ end
113
118
  start = Time.now
114
119
  ms = [Integer(@timeout * 1000), 1].max
115
120
  begin
@@ -118,14 +123,16 @@ class Pgtk::Impatient
118
123
  t.exec(sql, *args)
119
124
  end
120
125
  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(' '))
126
+ raise(
127
+ TooSlow, [
128
+ 'SQL query',
129
+ ("with #{args.count} argument#{'s' if args.count > 1}" unless args.empty?),
130
+ 'was terminated after',
131
+ start.ago,
132
+ 'of waiting:',
133
+ sql.ellipsized(50).inspect
134
+ ].compact.join(' ')
135
+ )
129
136
  end
130
137
  end
131
138
 
@@ -146,3 +153,5 @@ class Pgtk::Impatient
146
153
  end
147
154
  end
148
155
  end
156
+
157
+ 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,23 @@ 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
+ '--health-cmd', 'pg_isready',
133
+ '--health-interval', '1s',
134
+ '--health-timeout', '2s',
135
+ '--health-retries', '10',
136
+ 'postgres:18.1',
137
+ @config.map { |k, v| "-c #{Shellwords.escape("#{k}=#{v}")}" },
138
+ stdout:
139
+ ).scan(/[a-f0-9]+\Z/).first
140
140
  File.write(File.join(home, 'docker-container'), container)
141
141
  at_exit do
142
142
  if qbash(
@@ -152,6 +152,15 @@ class Pgtk::PgsqlTask < Rake::TaskLib
152
152
  rescue WaitUtil::TimeoutError => e
153
153
  raise(IOError, "Failed to start PostgreSQL Docker container #{container.inspect}: #{e.message}")
154
154
  end
155
+ (30 * 2).times do
156
+ status = qbash(
157
+ "docker inspect --format='{{.State.Health.Status}}' #{Shellwords.escape(container)}",
158
+ accept: nil, both: true
159
+ )[0]&.strip
160
+ break if status == 'healthy'
161
+ raise(IOError, "PostgreSQL container #{container.inspect} is #{status}") if status == 'unhealthy'
162
+ sleep(0.5)
163
+ end
155
164
  container
156
165
  end
157
166
 
@@ -185,13 +194,17 @@ class Pgtk::PgsqlTask < Rake::TaskLib
185
194
  puts("PostgreSQL killed in PID #{pid}") unless @quiet
186
195
  end
187
196
  begin
188
- WaitUtil.wait_for_service('PG in local', 'localhost', port, timeout_sec: 10, delay_sec: 0.1)
197
+ WaitUtil.wait_for_service('PG in local', 'localhost', port, timeout_sec: 10, delay_sec: 0.2)
189
198
  rescue WaitUtil::TimeoutError => e
190
199
  puts("+ #{cmd}")
191
200
  puts("stdout:\n#{File.read(File.join(home, 'stdout.txt'))}")
192
201
  puts("stderr:\n#{File.read(File.join(home, 'stderr.txt'))}")
193
202
  raise(IOError, "Failed to start PostgreSQL database server on port #{port}: #{e.message}")
194
203
  end
204
+ (10 * 2).times do
205
+ break if qbash("pg_isready -h localhost -p #{port}", accept: nil, both: true)[1].zero?
206
+ sleep(0.5)
207
+ end
195
208
  qbash(
196
209
  'createdb',
197
210
  '--host', 'localhost',
@@ -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